6 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
22 changed files with 526 additions and 542 deletions

2
.gitmodules vendored
View File

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

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -1,11 +1,11 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ModpackUpdater.Apps.Client.Gui.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ModpackUpdater.Apps.Client.Gui.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<FluentTheme/>
<StyleInclude Source="avares://Pilz.UI.AvaloniaUI/Assets/Styles/EnhancedDefaults.axaml"/>
</Application.Styles>
</Application>

View File

@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
namespace ModpackUpdater.Apps.Client.Gui;
@@ -15,7 +16,26 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainForm();
{
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainView
{
DataContext = new MainViewModel(),
};
}
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

@@ -1,379 +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 Pilz.Extensions;
using Pilz.Extensions.Collections;
using Pilz.UI.Symbols;
namespace ModpackUpdater.Apps.Client.Gui;
public partial class MainForm : Window
{
private readonly MenuFlyout menuFlyoutSearchProfileFolder = new();
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;
public MainForm()
{
InitializeComponent();
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortString()})";
ButtonSearchProfileFolder.Flyout = menuFlyoutSearchProfileFolder;
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 LoadRecentFilesToUi()
{
menuFlyoutSearchProfileFolder.Items.Clear();
if (AppConfig.Instance.RecentMinecraftProfilePaths.Count == 0)
{
menuFlyoutSearchProfileFolder.Items.Add(new TextBlock
{
Text = GeneralLangRes.NoRecentProfilesAvailable,
FontStyle = FontStyle.Italic,
});
return;
}
AppConfig.Instance.RecentMinecraftProfilePaths.ForEach(path =>
{
if (File.Exists(path))
return;
var item = new MenuItem
{
Header = path.Length > 50 ? $"...{path[^50..]}" : path,
DataContext = path,
};
item.Click += MenuItemRecentMinecraftProfilePathItem_Click;
menuFlyoutSearchProfileFolder.Items.Add(item);
});
}
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));
}
private void LoadProfileToUi()
{
loadingData = true;
TextBoxMinecraftProfileFolder.Text = modpackInfo.LocalPath ?? AppConfig.Instance.RecentMinecraftProfilePaths.FirstOrDefault() ?? 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))
return;
await ExecuteUpdate(false, false);
StoreRecentMinecraftProfilePath(modpackInfo.LocalPath);
LoadRecentFilesToUi();
}
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 async void MainForm_Loaded(object? sender, RoutedEventArgs e)
{
var updates = new AppUpdates("client", this);
updates.OnDownloadProgramUpdate += (_, _) => SetStatus(GeneralLangRes.DownloadProgramUpdate, AppGlobals.Symbols.GetImageSource(AppSymbols.software_installer));
await updates.UpdateApp();
ClearStatus();
LoadRecentFilesToUi();
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);
}
private void MenuItemRecentMinecraftProfilePathItem_Click(object? sender, RoutedEventArgs e)
{
if (sender is MenuItem item && item.DataContext is string path)
TextBoxMinecraftProfileFolder.Text = path;
}
#endregion
}

View File

@@ -4,16 +4,23 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:lang="clr-namespace:ModpackUpdater.Apps.Client.Gui.LangRes"
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"
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"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
CanMaximize="false"
Title="Minecraft Modpack Updater"
Icon="/Assets/app.ico"
Loaded="MainForm_Loaded">
Loaded="Control_OnLoaded">
<Design.DataContext>
<gui:MainViewModel/>
</Design.DataContext>
<Grid
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
@@ -22,6 +29,13 @@
Margin="3"
x:Name="MainGrid">
<Grid.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="HasInitialized"/>
<Binding Path="!IsUpdating"/>
</MultiBinding>
</Grid.IsEnabled>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="250"/>
@@ -31,11 +45,11 @@
<!-- Labels -->
<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="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}"/>
<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"/>
<TextBlock x:Name="TextStatus"/>
<Image Width="{x:Static symbols:SymbolGlobals.DefaultImageSmallSize}" Source="{Binding StatusImage}"/>
<TextBlock Text="{Binding StatusText}"/>
</StackPanel>
<!-- TextBoxes: Profile -->
@@ -46,7 +60,7 @@
Margin="3"
VerticalAlignment="Center"
Watermark="C:\..."
TextChanged="TextBoxMinecraftProfileFolder_TextChanged"/>
Text="{Binding MinecraftProfileFolder}"/>
<!-- TextBoxes: ModpackConfig -->
<TextBox
@@ -57,7 +71,7 @@
Margin="3"
VerticalAlignment="Center"
Watermark="https://..."
TextChanged="TextBoxModpackConfig_TextChanged"/>
Text="{Binding ModpackConfigUrl}"/>
<!-- TextBoxes: InstallKey -->
<TextBox
@@ -68,8 +82,8 @@
Margin="3"
VerticalAlignment="Center"
Watermark="XXXXX-YYYYY-ZZZZZ-AAAAA-BBBBB"
TextChanged="TextBoxInstallKey_TextChanged"
IsVisible="false"/>
Text="{Binding InstallKey}"
IsVisible="{Binding CanUseExtrasKey}"/>
<!-- Button: SearchProfileFolder -->
<pilz:ImageSplitButton
@@ -80,7 +94,28 @@
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
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 -->
<pilz:ImageButton
@@ -91,7 +126,8 @@
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Text="{x:Static lang:GeneralLangRes.CheckForUpdates}"
Click="ButtonCheckForUpdates_Click"/>
Command="{Binding CheckForUpdatesCommand}"
IsEnabled="{Binding CanUpdate}"/>
<!-- Button: Install -->
<pilz:ImageSplitButton
@@ -103,10 +139,12 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Text="{x:Static lang:GeneralLangRes.Install}"
Click="ButtonInstall_Click">
Command="{Binding InstallCommand}"
IsEnabled="{Binding CanUpdate}">
<SplitButton.Flyout>
<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>
</SplitButton.Flyout>
</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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pilz" Version="2.6.2" />
<PackageReference Include="Pilz.Configuration" Version="3.2.7" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" />
<PackageReference Include="Pilz.IO" Version="2.1.0" />
<PackageReference Include="Pilz.UI" Version="3.1.4" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.20" />
<PackageReference Include="Avalonia" Version="11.3.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
<PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.Configuration" Version="3.2.8" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.IO" Version="2.1.1" />
<PackageReference Include="Pilz.UI" Version="3.1.5" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.21" />
<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.Themes.Fluent" Version="11.3.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.9">
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Pilz.Updating" Version="4.3.5" />
<PackageReference Include="Pilz.Updating.Client" Version="4.4.6" />
<PackageReference Include="Pilz.Updating" Version="4.3.6" />
<PackageReference Include="Pilz.Updating.Client" Version="4.4.8" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -31,7 +31,7 @@ public static class Program
private static void DrawInfo()
{
Console.WriteLine("Minecraft Modpack Updater CLI");
Console.WriteLine("Version " + Assembly.GetExecutingAssembly().GetAppVersion().ToShortString());
Console.WriteLine("Version " + Assembly.GetExecutingAssembly().GetAppVersion().ToShortHumanString());
Console.WriteLine("------------------------------");
}
@@ -39,13 +39,12 @@ public static class Program
{
var info = ModpackInfo.TryLoad(updateOptions.ProfileFolder);
var config = ModpackConfig.LoadFromUrl(CheckModpackConfigUrl(updateOptions.ModpackConfig!, info));
var features = new ModpackFeatures(config);
// Check features
if (!string.IsNullOrWhiteSpace(updateOptions.ExtrasKey))
info.ExtrasKey = updateOptions.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
var installer = new ModpackInstaller(config, info)

View File

@@ -49,25 +49,25 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.9" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.10" />
<PackageReference Include="DynamicData" Version="9.4.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.0" />
<PackageReference Include="EPPlus" Version="8.3.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.3.1" />
<PackageReference Include="EPPlus" Version="8.4.0" />
<PackageReference Include="NGitLab" Version="11.1.0" />
<PackageReference Include="Pilz" Version="2.6.2" />
<PackageReference Include="Pilz.Configuration" Version="3.2.7" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.2" />
<PackageReference Include="Pilz.Features" Version="2.13.0" />
<PackageReference Include="Pilz.UI" Version="3.1.4" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.20" />
<PackageReference Include="Pilz.UI.AvaloniaUI.Features" Version="1.0.1" />
<PackageReference Include="Avalonia" Version="11.3.9" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
<PackageReference Include="Pilz" Version="2.7.8" />
<PackageReference Include="Pilz.Configuration" Version="3.2.8" />
<PackageReference Include="Pilz.Cryptography" Version="2.1.3" />
<PackageReference Include="Pilz.Features" Version="2.13.1" />
<PackageReference Include="Pilz.UI" Version="3.1.5" />
<PackageReference Include="Pilz.UI.AvaloniaUI" Version="1.2.21" />
<PackageReference Include="Pilz.UI.AvaloniaUI.Features" Version="1.0.2" />
<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.Themes.Fluent" Version="11.3.9" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.9" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.9">
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>

View File

@@ -30,7 +30,7 @@ public partial class MainWindow : Window, IMainApi
{
InitializeComponent();
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortString()})";
Title = $"{Title} (v{Assembly.GetExecutingAssembly().GetAppVersion().ToShortHumanString()})";
GridMain.DataContext = Model;

View File

@@ -12,7 +12,7 @@ namespace ModpackUpdater.Apps;
public class AppUpdates(string appShortName, Window mainWindow)
{
public const string UpdateUrl = "https://git.pilzinsel64.de/litw-refined/minecraft-modpack-updater/-/snippets/3/raw/main/updates-new.json";
public const string UpdateUrl = "https://git.pilzinsel64.de/LITW-Refined/minecraft-modpack-updater/raw/branch/updates/updates.json";
public event EventHandler? OnDownloadProgramUpdate;

View File

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

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)
{
var destFilePath = iaction.GetDestPath(modpackInfo.LocalPath);
var sourceUrl = updateConfig.PreferDirectLinks && !string.IsNullOrWhiteSpace(iaction.SourceUrl)
? iaction.GetSourceUrl(checkResult.LatestVersion, overwriteVersion: OverwriteVersion)
: await factory.ResolveSourceUrl(iaction, targetVersion: checkResult.LatestVersion, overwriteVersion: OverwriteVersion);
var sourceUrl = iaction.GetSourceUrl(checkResult.LatestVersion, overwriteVersion: OverwriteVersion);
if (iaction is UpdateAction uaction)
{

View File

@@ -9,11 +9,11 @@
<ItemGroup>
<PackageReference Include="Castle.Core" Version="5.2.1" />
<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="Octokit" Version="14.0.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>

View File

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

View File

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

View File

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

View File

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