This commit is contained in:
2025-11-17 15:02:47 +01:00
parent d80f5c47b1
commit 7e2a103dbe
17 changed files with 182 additions and 78 deletions

View File

@@ -8,4 +8,5 @@ public interface IMainApi
{ {
Window MainWindow { get; } Window MainWindow { get; }
MainWindowViewModel Model { get; } MainWindowViewModel Model { get; }
bool HasClosed { get; }
} }

View File

@@ -18,7 +18,7 @@ internal class ClearDirectLinkFeature : PluginFunction, IPluginFeatureProvider<C
protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params) protected override Task<object?> ExecuteFunctionAsync(PluginFunctionParameter? @params)
{ {
if (@params is MainApiParameters p && p.Api.Model.SelectedGridRow is { } row) if (@params is MainApiParameters p && p.Api.Model.SelectedGridRow is { } row)
SharedFunctions.ClearDirectLinks(row); SharedFunctions.ClearDirectLinks(p.Api, row);
return Task.FromResult<object?>(null); return Task.FromResult<object?>(null);
} }
} }

View File

@@ -21,7 +21,7 @@ internal class UpdateDirectLinkFeature : PluginFunction, IPluginFeatureProvider<
if (@params is not MainApiParameters p || p.Api.Model.SelectedGridRow is not MainWindowGridRow row) if (@params is not MainApiParameters p || p.Api.Model.SelectedGridRow is not MainWindowGridRow row)
return null; return null;
await SharedFunctions.FindNewDirectLinks(row); await SharedFunctions.FindNewDirectLinks(p.Api, row);
return null; return null;
} }

View File

@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using Avalonia.Media;
using ModpackUpdater.Apps.Manager.Api.Model; using ModpackUpdater.Apps.Manager.Api.Model;
using ModpackUpdater.Apps.Manager.LangRes; using ModpackUpdater.Apps.Manager.LangRes;
using ModpackUpdater.Apps.Manager.Ui; using ModpackUpdater.Apps.Manager.Ui;
@@ -14,16 +15,26 @@ namespace ModpackUpdater.Apps.Manager.Features;
internal static class SharedFunctions internal static class SharedFunctions
{ {
public static async Task<bool> CheckActionHealthy(IMainApi api, params MainWindowGridRow[] rows) private static readonly IImage? imageSourceSuccess = AppGlobals.Symbols.GetImageSource(AppSymbols.done);
private static readonly IImage? imageSourceWorking = AppGlobals.Symbols.GetImageSource(AppSymbols.hourglass);
private static readonly IImage? imageSourceFailed = AppGlobals.Symbols.GetImageSource(AppSymbols.close);
public static async Task CheckActionHealthy(IMainApi api, params MainWindowGridRow[] rows)
{ {
var rowsCount = rows.Length; var rowsCount = rows.Length;
var failed = false; var failed = false;
var msg = default(string);
var factory = new ModpackFactory(); var factory = new ModpackFactory();
api.Model.Progress.Value = 0;
api.Model.Progress.MaxValue = rowsCount;
api.Model.Progress.Visible = true;
for (var i = 0; i < rows.Length; i++) for (var i = 0; i < rowsCount; i++)
{ {
var row = rows[i]; var row = rows[i];
if (row.SourceType == SourceType.DirectLink)
continue;
row.StateImage = imageSourceWorking;
try try
{ {
@@ -32,18 +43,17 @@ internal static class SharedFunctions
} }
catch (Exception ex) catch (Exception ex)
{ {
msg = ex.Message; // Ignore
} }
if (api.HasClosed)
return;
row.IsValid = !failed; row.StateImage = failed ? imageSourceFailed : imageSourceSuccess;
api.Model.Progress.Value = i;
// rwb.Text = $"{i} / {rowsCount}";
} }
if (rowsCount == 1 && failed && !string.IsNullOrWhiteSpace(msg)) api.Model.Progress.Visible = false;
_ = MessageBoxManager.GetMessageBoxStandard(string.Empty, msg).ShowAsPopupAsync(api.MainWindow);
return true;
} }
public static async Task<bool> CollectUpdates(IMainApi api, params InstallAction[] actions) public static async Task<bool> CollectUpdates(IMainApi api, params InstallAction[] actions)
@@ -53,7 +63,7 @@ internal static class SharedFunctions
// Collect updates // Collect updates
var result = await AvaloniaFlyoutBase.Show(new UpdatesCollectorView(api.Model.CurrentWorkspace, actions), api.MainWindow); var result = await AvaloniaFlyoutBase.Show(new UpdatesCollectorView(api.Model.CurrentWorkspace, actions), api.MainWindow);
if (result.Result is not ModUpdates resultUpdates) if (api.HasClosed || result.Result is not ModUpdates resultUpdates)
return false; return false;
// Collect versions with changes // Collect versions with changes
@@ -86,33 +96,55 @@ internal static class SharedFunctions
return true; return true;
} }
public static async Task FindNewDirectLinks(params MainWindowGridRow[] rows) public static async Task FindNewDirectLinks(IMainApi api, params MainWindowGridRow[] rows)
{ {
var factory = new ModpackFactory(); var factory = new ModpackFactory();
foreach (var row in rows) api.Model.Progress.Value = 0;
api.Model.Progress.MaxValue = rows.Length;
api.Model.Progress.Visible = true;
for (var i = 0; i < rows.Length; i++)
{ {
var row = rows[i];
if (row.SourceType == SourceType.DirectLink) if (row.SourceType == SourceType.DirectLink)
continue; continue;
row.StateImage = imageSourceWorking;
try try
{ {
row.SourceUrl = await factory.ResolveSourceUrl(row.Action); row.SourceUrl = await factory.ResolveSourceUrl(row.Action);
row.StateImage = imageSourceSuccess;
} }
catch (Exception) catch (Exception)
{ {
// Fail silently row.StateImage = imageSourceFailed;
} }
if (api.HasClosed)
return;
api.Model.Progress.Value = i;
} }
api.Model.Progress.Visible = false;
} }
public static void ClearDirectLinks(params MainWindowGridRow[] rows) public static void ClearDirectLinks(IMainApi api, params MainWindowGridRow[] rows)
{ {
foreach (var row in rows) api.Model.Progress.Value = 0;
api.Model.Progress.MaxValue = rows.Length;
api.Model.Progress.Visible = true;
for (var i = 0; i < rows.Length; i++)
{ {
var row = rows[i];
if (row.SourceType != SourceType.DirectLink) if (row.SourceType != SourceType.DirectLink)
row.SourceUrl = null; row.SourceUrl = null;
row.StateImage = null;
api.Model.Progress.Value = i;
} }
api.Model.Progress.Visible = false;
} }
public static string GenerateChangelog(InstallInfos installInfos, UpdateInfo updateInfos) public static string GenerateChangelog(InstallInfos installInfos, UpdateInfo updateInfos)

View File

@@ -18,7 +18,7 @@ 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 && p.Api.Model.CurrentGridRows is not null)
SharedFunctions.ClearDirectLinks([.. p.Api.Model.CurrentGridRows]); SharedFunctions.ClearDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]);
return Task.FromResult<object?>(null); return Task.FromResult<object?>(null);
} }
} }

View File

@@ -20,7 +20,7 @@ internal class UpdateDirectLinksFeature : PluginFunction, IPluginFeatureProvider
if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null) if (@params is not MainApiParameters p || p.Api.Model.CurrentGridRows is null)
return null; return null;
await SharedFunctions.FindNewDirectLinks([.. p.Api.Model.CurrentGridRows]); await SharedFunctions.FindNewDirectLinks(p.Api, [.. p.Api.Model.CurrentGridRows]);
return null; return null;
} }

View File

@@ -195,6 +195,12 @@ namespace ModpackUpdater.Apps.Manager.LangRes {
} }
} }
public static string State {
get {
return ResourceManager.GetString("State", resourceCulture);
}
}
public static string InheritFrom { public static string InheritFrom {
get { get {
return ResourceManager.GetString("InheritFrom", resourceCulture); return ResourceManager.GetString("InheritFrom", resourceCulture);

View File

@@ -192,6 +192,9 @@
<data name="DestinationPath" xml:space="preserve"> <data name="DestinationPath" xml:space="preserve">
<value>Destination path</value> <value>Destination path</value>
</data> </data>
<data name="State" xml:space="preserve">
<value>State</value>
</data>
<data name="InheritFrom" xml:space="preserve"> <data name="InheritFrom" xml:space="preserve">
<value>Inherit from</value> <value>Inherit from</value>
</data> </data>

View File

@@ -13,7 +13,8 @@
x:DataType="mainWindow:MainWindowViewModel" x:DataType="mainWindow:MainWindowViewModel"
Title="Minecraft Modpack Manager" Title="Minecraft Modpack Manager"
WindowState="Maximized" WindowState="Maximized"
Loaded="Window_OnLoaded"> Loaded="Window_OnLoaded"
Closed="Window_OnClosed">
<Grid <Grid
x:Name="GridMain" x:Name="GridMain"
@@ -100,52 +101,80 @@
</ScrollViewer> </ScrollViewer>
<!-- Panel: List header --> <!-- Panel: List header -->
<ContentControl <StackPanel
Grid.Column="1" Grid.Column="1"
Grid.Row="0" Grid.Row="0"
Content="{Binding SelectedTreeNode}"> Spacing="6"
Orientation="Horizontal"
VerticalAlignment="Center">
<ContentControl.DataTemplates> <!-- Panel: Menu -->
<DataTemplate <ContentControl
DataType="mainWindow:ActionSetTreeNode"> Content="{Binding SelectedTreeNode}">
<StackPanel <ContentControl.DataTemplates>
Orientation="Horizontal"> <DataTemplate
DataType="mainWindow:ActionSetTreeNode">
<!-- Button: Add action --> <StackPanel
<pilz:ImageButton Orientation="Horizontal">
x:Name="ButtonAddAction"
Text="{x:Static langRes:GeneralLangRes.Add}"
ImageSource="{x:Static local:MainWindow.ButtonImageAddAction}"
Background="Transparent"
Click="ButtonAddAction_OnClick"/>
<!-- Button: Remove action --> <!-- Button: Add action -->
<pilz:ImageButton <pilz:ImageButton
x:Name="ButtonRemoveAction" x:Name="ButtonAddAction"
Text="{x:Static langRes:GeneralLangRes.Remove}" Text="{x:Static langRes:GeneralLangRes.Add}"
ImageSource="{x:Static local:MainWindow.ButtonImageRemoveAction}" ImageSource="{x:Static local:MainWindow.ButtonImageAddAction}"
Background="Transparent" Background="Transparent"
Click="ButtonRemoveAction_OnClick"/> Click="ButtonAddAction_OnClick"/>
<!-- TextBox: Version --> <!-- Button: Remove action -->
<TextBox <pilz:ImageButton
Margin="3, 0, 3, 0" x:Name="ButtonRemoveAction"
Width="100" Text="{x:Static langRes:GeneralLangRes.Remove}"
Text="{Binding Version}"/> ImageSource="{x:Static local:MainWindow.ButtonImageRemoveAction}"
Background="Transparent"
Click="ButtonRemoveAction_OnClick"/>
<!-- CheckBox: Is public --> <!-- TextBox: Version -->
<CheckBox <TextBox
Margin="3, 0, 3, 0" Margin="3, 0, 3, 0"
Content="{x:Static langRes:GeneralLangRes.Public}" Width="100"
IsChecked="{Binding IsPublic}"/> Text="{Binding Version}"/>
</StackPanel>
</DataTemplate>
<DataTemplate <!-- CheckBox: Is public -->
DataType="mainWindow:MainWindowTreeNode"/> <CheckBox
</ContentControl.DataTemplates> Margin="3, 0, 3, 0"
</ContentControl> Content="{x:Static langRes:GeneralLangRes.Public}"
IsChecked="{Binding IsPublic}"/>
</StackPanel>
</DataTemplate>
<DataTemplate
DataType="mainWindow:MainWindowTreeNode"/>
</ContentControl.DataTemplates>
</ContentControl>
<!-- Progress -->
<Panel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding Progress.Visible}">
<!-- ProgressBar: Status -->
<ProgressBar
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Maximum="{Binding Progress.MaxValue}"
Value="{Binding Progress.Value}"/>
<!-- TextBox: Status -->
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
Text="{Binding Progress.Text}"/>
</Panel>
</StackPanel>
<!-- DataGrid --> <!-- DataGrid -->
<DataGrid <DataGrid
@@ -155,7 +184,7 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding CurrentGridRows}" ItemsSource="{Binding CurrentGridRows}"
SelectedItem="{Binding SelectedGridRow}"> SelectedItem="{Binding SelectedGridRow}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.Id}" Header="{x:Static langRes:GeneralLangRes.Id}"
@@ -177,6 +206,18 @@
<DataGridTextColumn <DataGridTextColumn
Header="{x:Static langRes:GeneralLangRes.DestinationPath}" Header="{x:Static langRes:GeneralLangRes.DestinationPath}"
Binding="{Binding InheritedDestPath}"/> Binding="{Binding InheritedDestPath}"/>
<DataGridTemplateColumn
Header="{x:Static langRes:GeneralLangRes.State}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image
Width="{x:Static symbols:SymbolGlobals.DefaultImageSmallSize}"
Source="{Binding StateImage}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using ModpackUpdater.Apps.Manager.Api; using ModpackUpdater.Apps.Manager.Api;
@@ -22,6 +23,7 @@ public partial class MainWindow : Window, IMainApi
public MainWindowViewModel Model { get; } = new(); public MainWindowViewModel Model { get; } = new();
Window IMainApi.MainWindow => this; Window IMainApi.MainWindow => this;
public IMainApi MainApi => this; public IMainApi MainApi => this;
public bool HasClosed { get; private set; }
public MainWindow() public MainWindow()
{ {
@@ -110,6 +112,11 @@ public partial class MainWindow : Window, IMainApi
LoadRecentWorkspaces(); LoadRecentWorkspaces();
} }
private void Window_OnClosed(object? sender, EventArgs e)
{
HasClosed = true;
}
private async void MenuItemNewWorkspaceItem_Click(object? sender, RoutedEventArgs e) private async void MenuItemNewWorkspaceItem_Click(object? sender, RoutedEventArgs e)
{ {
if (sender is not MenuItem item || item.Tag is not WorkspaceFeature feature) if (sender is not MenuItem item || item.Tag is not WorkspaceFeature feature)

View File

@@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Media;
using ModpackUpdater.Apps.Manager.LangRes; using ModpackUpdater.Apps.Manager.LangRes;
using ModpackUpdater.Apps.Manager.Utils; using ModpackUpdater.Apps.Manager.Utils;
using PropertyChanged; using PropertyChanged;
@@ -18,7 +19,7 @@ public class MainWindowGridRow(InstallAction action, IActionSet baseActions) : I
private InstallAction Inherited => Base ?? action; private InstallAction Inherited => Base ?? action;
public bool IsUpdate => action is UpdateAction; public bool IsUpdate => action is UpdateAction;
public bool? IsValid { get; set; } = null; public IImage? StateImage { get; set; }
[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;

View File

@@ -16,7 +16,7 @@ public class MainWindowViewModel : INotifyPropertyChanged
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 MainWindowGridRow? SelectedGridRow { get; set; } public MainWindowGridRow? SelectedGridRow { get; set; }
[AlsoNotifyFor(nameof(CurrentTreeNodes))] [AlsoNotifyFor(nameof(CurrentTreeNodes))]

View File

@@ -0,0 +1,15 @@
using System.ComponentModel;
using PropertyChanged;
namespace ModpackUpdater.Apps.Manager.Ui.Models;
public class ProgressInfos : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public bool Visible { get; set; }
public double MaxValue { get; set; }
public double Value { get; set; }
[DependsOn(nameof(Value), nameof(MaxValue))]
public string? Text => $"{Math.Round(Value / MaxValue * 100)}%";
}

View File

@@ -7,12 +7,8 @@ namespace ModpackUpdater.Apps.Manager.Ui.Models.UpdatesCollectorViewMode;
public class UpdatesCollectorViewModel : INotifyPropertyChanged public class UpdatesCollectorViewModel : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public bool ProgressVisible { get; set; } public ProgressInfos Progress { get; } = new();
public double ProgressMaxValue { get; set; }
public double ProgressValue { get; set; }
[DependsOn(nameof(ProgressValue), nameof(ProgressMaxValue))]
public string? ProgressText => $"{Math.Round(ProgressValue / ProgressMaxValue * 100)}%";
public string? SearchText { get; set; } public string? SearchText { get; set; }
public ObservableCollection<ModUpdateInfo> Updates { get; } = []; public ObservableCollection<ModUpdateInfo> Updates { get; } = [];
} }

View File

@@ -85,20 +85,20 @@
<Panel <Panel
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
IsVisible="{Binding ProgressVisible}"> IsVisible="{Binding Progress.Visible}">
<!-- ProgressBar: Status --> <!-- ProgressBar: Status -->
<ProgressBar <ProgressBar
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Maximum="{Binding ProgressMaxValue}" Maximum="{Binding Progress.MaxValue}"
Value="{Binding ProgressValue}"/> Value="{Binding Progress.Value}"/>
<!-- TextBox: Status --> <!-- TextBox: Status -->
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding ProgressText}"/> Text="{Binding Progress.Text}"/>
</Panel> </Panel>
</dialogs:AvaloniaFlyoutBase.FooterContent> </dialogs:AvaloniaFlyoutBase.FooterContent>
</dialogs:AvaloniaFlyoutBase> </dialogs:AvaloniaFlyoutBase>

View File

@@ -25,14 +25,15 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
private async Task FindUpdates() private async Task FindUpdates()
{ {
Model.ProgressVisible = true; Model.Progress.Value = 0;
Model.ProgressMaxValue = actions.Length; Model.Progress.MaxValue = actions.Length;
Model.Progress.Visible = true;
foreach (var action in actions) foreach (var action in actions)
{ {
var updates = await factory.FindUpdates(action, workspace.ModpackConfig?.MinecraftVersion, workspace.ModpackConfig?.ModLoader ?? ModLoader.Any); var updates = await factory.FindUpdates(action, workspace.ModpackConfig?.MinecraftVersion, workspace.ModpackConfig?.ModLoader ?? ModLoader.Any);
Model.ProgressValue += 1; Model.Progress.Value += 1;
if (updates == null || updates.Length == 0 || updates[0].Value == action.SourceTag) if (updates == null || updates.Length == 0 || updates[0].Value == action.SourceTag)
continue; continue;
@@ -43,7 +44,7 @@ public partial class UpdatesCollectorView : AvaloniaFlyoutBase
break; break;
} }
Model.ProgressVisible = false; Model.Progress.Visible = false;
} }
protected override object GetResult() protected override object GetResult()

View File

@@ -41,4 +41,5 @@ public enum AppSymbols
input, input,
output, output,
git, git,
hourglass,
} }