Merge pull request #503 from 4sval/no-update-modes
Some checks failed
FModel QA Builder / build (push) Has been cancelled

No update modes
This commit is contained in:
Valentin 2024-10-06 16:58:36 +02:00 committed by GitHub
commit 7fda6bc3b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 817 additions and 47 deletions

View File

@ -106,7 +106,7 @@ public partial class App
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [FModel] [{Level:u3}] {Message:lj}{NewLine}{Exception}").CreateLogger();
#endif
Log.Information("Version {Version}", Constants.APP_VERSION);
Log.Information("Version {Version} ({CommitId})", Constants.APP_VERSION, Constants.APP_COMMIT_ID);
Log.Information("{OS}", GetOperatingSystemProductName());
Log.Information("{RuntimeVer}", RuntimeInformation.FrameworkDescription);
Log.Information("Culture {SysLang}", CultureInfo.CurrentCulture);

View File

@ -1,12 +1,20 @@
using System.Numerics;
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Reflection;
using CUE4Parse.UE4.Objects.Core.Misc;
using FModel.Extensions;
namespace FModel;
public static class Constants
{
public static readonly string APP_VERSION = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
public static readonly string APP_PATH = Path.GetFullPath(Environment.GetCommandLineArgs()[0]);
public static readonly string APP_VERSION = FileVersionInfo.GetVersionInfo(APP_PATH).FileVersion;
public static readonly string APP_COMMIT_ID = FileVersionInfo.GetVersionInfo(APP_PATH).ProductVersion.SubstringAfter('+');
public static readonly string APP_SHORT_COMMIT_ID = APP_COMMIT_ID[..7];
public const string ZERO_64_CHAR = "0000000000000000000000000000000000000000000000000000000000000000";
public static readonly FGuid ZERO_GUID = new(0U);
@ -21,6 +29,9 @@ public static class Constants
public const string BLUE = "#528BCC";
public const string ISSUE_LINK = "https://github.com/4sval/FModel/discussions/categories/q-a";
public const string GH_REPO = "https://api.github.com/repos/4sval/FModel";
public const string GH_COMMITS_HISTORY = GH_REPO + "/commits";
public const string GH_RELEASES = GH_REPO + "/releases";
public const string DONATE_LINK = "https://fmodel.app/donate";
public const string DISCORD_LINK = "https://fmodel.app/discord";

View File

@ -147,11 +147,11 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Changelog" Command="{Binding MenuCommand}" CommandParameter="Help_Changelog">
<MenuItem Header="Releases" Command="{Binding MenuCommand}" CommandParameter="Help_Releases">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource NoteIcon}" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}" Data="{StaticResource GitHubIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
@ -797,13 +797,17 @@
<Style TargetType="StatusBarItem">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAutoOpenSounds, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Hidden" />
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock HorizontalAlignment="Center" FontWeight="SemiBold" Text="SND" />
</StatusBarItem>
<StatusBarItem Margin="10 0 0 0">
<TextBlock Text="{Binding LastUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, Converter={x:Static converters:RelativeDateTimeConverter.Instance}, StringFormat=Last Refresh: {0}}" />
</StatusBarItem>
</StackPanel>
</StatusBarItem>
</StatusBar>

View File

@ -186,11 +186,18 @@ namespace FModel.Settings
set => SetProperty(ref _updateMode, value);
}
private string _commitHash = Constants.APP_VERSION;
public string CommitHash
private DateTime _lastUpdateCheck = DateTime.MinValue;
public DateTime LastUpdateCheck
{
get => _commitHash;
set => SetProperty(ref _commitHash, value);
get => _lastUpdateCheck;
set => SetProperty(ref _lastUpdateCheck, value);
}
private DateTime _nextUpdateCheck = DateTime.Now;
public DateTime NextUpdateCheck
{
get => _nextUpdateCheck;
set => SetProperty(ref _nextUpdateCheck, value);
}
private bool _keepDirectoryStructure = true;
@ -265,8 +272,6 @@ namespace FModel.Settings
[JsonIgnore]
public DirectorySettings CurrentDir { get; set; }
[JsonIgnore]
public string ShortCommitHash => CommitHash[..7];
/// <summary>
/// TO DELETEEEEEEEEEEEEE

View File

@ -20,6 +20,7 @@ public class ApiEndpointViewModel
public FortniteCentralApiEndpoint CentralApi { get; }
public EpicApiEndpoint EpicApi { get; }
public FModelApiEndpoint FModelApi { get; }
public GitHubApiEndpoint GitHubApi { get; }
public DynamicApiEndpoint DynamicApi { get; }
public ApiEndpointViewModel()
@ -29,6 +30,7 @@ public class ApiEndpointViewModel
CentralApi = new FortniteCentralApiEndpoint(_client);
EpicApi = new EpicApiEndpoint(_client);
FModelApi = new FModelApiEndpoint(_client);
GitHubApi = new GitHubApiEndpoint(_client);
DynamicApi = new DynamicApiEndpoint(_client);
}

View File

@ -10,6 +10,7 @@ using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using FModel.Views;
using Newtonsoft.Json;
using RestSharp;
using Serilog;
@ -118,6 +119,8 @@ public class FModelApiEndpoint : AbstractApiProvider
public void CheckForUpdates(EUpdateMode updateMode, bool launch = false)
{
if (DateTime.Now < UserSettings.Default.NextUpdateCheck) return;
if (launch)
{
AutoUpdater.ParseUpdateInfoEvent += ParseUpdateInfoEvent;
@ -149,43 +152,20 @@ public class FModelApiEndpoint : AbstractApiProvider
{
if (args is { CurrentVersion: { } })
{
UserSettings.Default.LastUpdateCheck = DateTime.Now;
var qa = (CustomMandatory) args.Mandatory;
var currentVersion = new System.Version(args.CurrentVersion);
if ((qa.Value && qa.CommitHash == UserSettings.Default.CommitHash) || // qa branch : same commit id
(!qa.Value && currentVersion == args.InstalledVersion && args.CurrentVersion == UserSettings.Default.CommitHash)) // stable - beta branch : same version + commit id = version
if ((qa.Value && qa.CommitHash == Constants.APP_COMMIT_ID) || // qa branch : same commit id
(!qa.Value && currentVersion == args.InstalledVersion)) // stable - beta branch : same version + commit id = version
{
if (UserSettings.Default.ShowChangelog)
ShowChangelog(args);
return;
}
var downgrade = currentVersion < args.InstalledVersion;
var messageBox = new MessageBoxModel
{
Text = $"The latest version of FModel {UserSettings.Default.UpdateMode.GetDescription()} is {(qa.Value ? qa.ShortCommitHash : args.CurrentVersion)}. You are using version {(qa.Value ? UserSettings.Default.ShortCommitHash : args.InstalledVersion)}. Do you want to {(downgrade ? "downgrade" : "update")} the application now?",
Caption = $"{(downgrade ? "Downgrade" : "Update")} Available",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes) return;
try
{
if (AutoUpdater.DownloadUpdate(args))
{
UserSettings.Default.ShowChangelog = currentVersion != args.InstalledVersion;
UserSettings.Default.CommitHash = qa.CommitHash;
Application.Current.Shutdown();
}
}
catch (Exception exception)
{
UserSettings.Default.ShowChangelog = false;
MessageBox.Show(exception.Message, exception.GetType().ToString(), MessageBoxButton.OK, MessageBoxImage.Error);
}
const string message = "A new update is available!";
Helper.OpenWindow<AdonisWindow>(message, () => new UpdateView { Title = message, ResizeMode = ResizeMode.NoResize }.ShowDialog());
}
else
{

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using FModel.Framework;
using FModel.ViewModels.ApiEndpoints.Models;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
public class GitHubApiEndpoint : AbstractApiProvider
{
private GitHubCommit[] _commits;
public GitHubApiEndpoint(RestClient client) : base(client) { }
public async Task<GitHubCommit[]> GetCommitHistoryAsync(string branch = "dev", int page = 1, int limit = 20)
{
var request = new FRestRequest(Constants.GH_COMMITS_HISTORY);
request.AddParameter("sha", branch);
request.AddParameter("page", page);
request.AddParameter("per_page", limit);
var response = await _client.ExecuteAsync<GitHubCommit[]>(request).ConfigureAwait(false);
return response.Data;
}
public async Task<GitHubRelease> GetReleaseAsync(string tag)
{
var request = new FRestRequest($"{Constants.GH_RELEASES}/tags/{tag}");
var response = await _client.ExecuteAsync<GitHubRelease>(request).ConfigureAwait(false);
return response.Data;
}
}

View File

@ -0,0 +1,125 @@
using System;
using System.Windows;
using AdonisUI.Controls;
using AutoUpdaterDotNET;
using FModel.Framework;
using FModel.Settings;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButton = AdonisUI.Controls.MessageBoxButton;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
using J = Newtonsoft.Json.JsonPropertyAttribute;
namespace FModel.ViewModels.ApiEndpoints.Models;
public class GitHubRelease
{
[J("assets")] public GitHubAsset[] Assets { get; private set; }
}
public class GitHubAsset : ViewModel
{
[J("name")] public string Name { get; private set; }
[J("size")] public int Size { get; private set; }
[J("download_count")] public int DownloadCount { get; private set; }
[J("browser_download_url")] public string BrowserDownloadUrl { get; private set; }
[J("created_at")] public DateTime CreatedAt { get; private set; }
[J("uploader")] public Author Uploader { get; private set; }
private bool _isLatest;
public bool IsLatest
{
get => _isLatest;
set => SetProperty(ref _isLatest, value);
}
}
public class GitHubCommit : ViewModel
{
private string _sha;
[J("sha")]
public string Sha
{
get => _sha;
set
{
SetProperty(ref _sha, value);
RaisePropertyChanged(nameof(IsCurrent));
RaisePropertyChanged(nameof(ShortSha));
}
}
[J("commit")] public Commit Commit { get; set; }
[J("author")] public Author Author { get; set; }
private GitHubAsset _asset;
public GitHubAsset Asset
{
get => _asset;
set
{
SetProperty(ref _asset, value);
RaisePropertyChanged(nameof(IsDownloadable));
}
}
public bool IsCurrent => Sha == Constants.APP_COMMIT_ID;
public string ShortSha => Sha[..7];
public bool IsDownloadable => Asset != null;
public void Download()
{
if (IsCurrent)
{
MessageBox.Show(new MessageBoxModel
{
Text = "You are already on the latest version.",
Caption = "Update FModel",
Icon = MessageBoxImage.Information,
Buttons = [MessageBoxButtons.Ok()],
IsSoundEnabled = false
});
return;
}
var messageBox = new MessageBoxModel
{
Text = $"Are you sure you want to update to version '{ShortSha}'?{(!Asset.IsLatest ? "\nThis is not the latest version." : "")}",
Caption = "Update FModel",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes) return;
try
{
if (AutoUpdater.DownloadUpdate(new UpdateInfoEventArgs { DownloadURL = Asset.BrowserDownloadUrl }))
{
Application.Current.Shutdown();
}
}
catch (Exception exception)
{
UserSettings.Default.ShowChangelog = false;
MessageBox.Show(exception.Message, exception.GetType().ToString(), MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
public class Commit
{
[J("author")] public Author Author { get; set; }
[J("message")] public string Message { get; set; }
}
public class Author
{
[J("name")] public string Name { get; set; }
[J("login")] public string Login { get; set; }
[J("date")] public DateTime Date { get; set; }
[J("avatar_url")] public string AvatarUrl { get; set; }
[J("html_url")] public string HtmlUrl { get; set; }
}

View File

@ -10,7 +10,6 @@ using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.VirtualFileSystem;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
@ -50,7 +49,7 @@ public class ApplicationViewModel : ViewModel
public CopyCommand CopyCommand => _copyCommand ??= new CopyCommand(this);
private CopyCommand _copyCommand;
public string InitialWindowTitle => $"FModel {UserSettings.Default.UpdateMode.GetDescription()}";
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID})";
public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}";
@ -144,7 +143,7 @@ public class ApplicationViewModel : ViewModel
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"\"{Path.GetFullPath(Environment.GetCommandLineArgs()[0])}\"",
Arguments = $"\"{path}\"",
UseShellExecute = false,
RedirectStandardOutput = false,
RedirectStandardError = false,

View File

@ -54,9 +54,8 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
case "Help_Donate":
Process.Start(new ProcessStartInfo { FileName = Constants.DONATE_LINK, UseShellExecute = true });
break;
case "Help_Changelog":
UserSettings.Default.ShowChangelog = true;
ApplicationService.ApiEndpointView.FModelApi.CheckForUpdates(UserSettings.Default.UpdateMode);
case "Help_Releases":
Helper.OpenWindow<AdonisWindow>("Releases", () => new UpdateView().Show());
break;
case "Help_BugsReport":
Process.Start(new ProcessStartInfo { FileName = Constants.ISSUE_LINK, UseShellExecute = true });

View File

@ -0,0 +1,40 @@
using System;
using FModel.Framework;
using FModel.Settings;
namespace FModel.ViewModels.Commands;
public class RemindMeCommand : ViewModelCommand<UpdateViewModel>
{
public RemindMeCommand(UpdateViewModel contextViewModel) : base(contextViewModel)
{
}
public override void Execute(UpdateViewModel contextViewModel, object parameter)
{
switch (parameter)
{
case "Days":
// check for update in 3 days
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(3);
break;
case "Week":
// check for update next week (a week starts on Monday)
var delay = DayOfWeek.Monday - DateTime.Now.DayOfWeek;
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(delay);
break;
case "Month":
// check for update next month (if today is 31st, it will be 1st of next month)
UserSettings.Default.NextUpdateCheck = DateTime.Now.AddDays(1 - DateTime.Now.Day).AddMonths(1);
break;
case "Never":
// never check for updates
UserSettings.Default.NextUpdateCheck = DateTime.MaxValue;
break;
default:
// reset
UserSettings.Default.NextUpdateCheck = DateTime.Now;
break;
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Data;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels.ApiEndpoints.Models;
using FModel.ViewModels.Commands;
using FModel.Views.Resources.Converters;
namespace FModel.ViewModels;
public class UpdateViewModel : ViewModel
{
private ApiEndpointViewModel _apiEndpointView => ApplicationService.ApiEndpointView;
private RemindMeCommand _remindMeCommand;
public RemindMeCommand RemindMeCommand => _remindMeCommand ??= new RemindMeCommand(this);
public RangeObservableCollection<GitHubCommit> Commits { get; }
public ICollectionView CommitsView { get; }
public UpdateViewModel()
{
Commits = new RangeObservableCollection<GitHubCommit>();
CommitsView = new ListCollectionView(Commits)
{
GroupDescriptions = { new PropertyGroupDescription("Commit.Author.Date", new DateTimeToDateConverter()) }
};
if (UserSettings.Default.NextUpdateCheck < DateTime.Now)
RemindMeCommand.Execute(this, null);
}
public async Task Load()
{
Commits.AddRange(await _apiEndpointView.GitHubApi.GetCommitHistoryAsync());
var qa = await _apiEndpointView.GitHubApi.GetReleaseAsync("qa");
qa.Assets.OrderByDescending(x => x.CreatedAt).First().IsLatest = true;
foreach (var asset in qa.Assets)
{
var commitSha = asset.Name.SubstringBeforeLast(".zip");
var commit = Commits.FirstOrDefault(x => x.Sha == commitSha);
if (commit != null)
{
commit.Asset = asset;
}
else
{
Commits.Add(new GitHubCommit
{
Sha = commitSha,
Commit = new Commit
{
Message = "No commit message",
Author = new Author { Name = asset.Uploader.Login, Date = asset.CreatedAt }
},
Author = asset.Uploader,
Asset = asset
});
}
}
}
public void DownloadLatest()
{
Commits.FirstOrDefault(x => x.Asset.IsLatest)?.Download();
}
}

View File

@ -0,0 +1,83 @@
<UserControl x:Class="FModel.Views.Resources.Controls.CommitControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border BorderThickness="1" CornerRadius="0.5"
BorderBrush="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Title}" FontWeight="Bold" />
<TextBlock Grid.Row="1" Text="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Description}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Commit.Message, Converter={x:Static converters:CommitMessageConverter.Instance}, ConverterParameter=Description}" Value="">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Author.AvatarUrl}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="2" FontSize="11">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} committed {1}">
<Binding Path="Commit.Author.Name" />
<Binding Path="Commit.Author.Date" Converter="{x:Static converters:RelativeDateTimeConverter.Instance}" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</Grid>
<controls:CommitDownloaderControl Grid.Column="1" Commit="{Binding}">
<controls:CommitDownloaderControl.Style>
<Style TargetType="controls:CommitDownloaderControl">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsDownloadable}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</controls:CommitDownloaderControl.Style>
</controls:CommitDownloaderControl>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace FModel.Views.Resources.Controls;
public partial class CommitControl : UserControl
{
public CommitControl()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,97 @@
<UserControl x:Class="FModel.Views.Resources.Controls.CommitDownloaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
BorderThickness="1"
CornerRadius="2.5"
Padding="5,2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock FontSize="9" Foreground="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=Border}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Asset.IsLatest}" Value="True">
<Setter Property="Text" Value="Latest" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Text" Value="Current" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Asset.IsLatest}" Value="True">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="BorderBrush" Value="#3fb950" />
<Setter Property="Background" Value="#0f3fb950" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCurrent}" Value="True">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="BorderBrush" Value="#3f92b9" />
<Setter Property="Background" Value="#0f3f92b9" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Grid Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Viewbox Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="{StaticResource ArchiveIcon}" />
</Canvas>
</Viewbox>
<StackPanel Grid.Column="2">
<TextBlock Text="Size" FontSize="10" />
<TextBlock FontSize="10" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}}" />
</StackPanel>
</Grid>
<Button Grid.Column="4" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Download"
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"
IsEnabled="{Binding IsCurrent, Converter={x:Static converters:InvertBooleanConverter.Instance}}"
Click="OnDownload">
<Viewbox Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Data="M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z" />
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,28 @@
using System.Windows;
using System.Windows.Controls;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.Views.Resources.Controls;
public partial class CommitDownloaderControl : UserControl
{
public CommitDownloaderControl()
{
InitializeComponent();
}
public static readonly DependencyProperty CommitProperty =
DependencyProperty.Register(nameof(Commit), typeof(GitHubCommit), typeof(CommitDownloaderControl), new PropertyMetadata(null));
public GitHubCommit Commit
{
get { return (GitHubCommit)GetValue(CommitProperty); }
set { SetValue(CommitProperty, value); }
}
private void OnDownload(object sender, RoutedEventArgs e)
{
Commit.Download();
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public class CommitMessageConverter : IValueConverter
{
public static readonly CommitMessageConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string commitMessage)
{
var parts = commitMessage.Split("\n\n");
return parameter?.ToString() == "Title" ? parts[0] : parts.Length > 1 ? parts[1] : string.Empty;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public class DateTimeToDateConverter : IValueConverter
{
public static readonly DateTimeToDateConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dateTime)
{
return DateOnly.FromDateTime(dateTime);
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public class InvertBooleanConverter : IValueConverter
{
public static readonly InvertBooleanConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolean)
{
return !boolean;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public class RelativeDateTimeConverter : IValueConverter
{
public static readonly RelativeDateTimeConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dateTime)
{
var timeSpan = DateTime.Now - dateTime.ToLocalTime();
int time;
string unit;
if (timeSpan.TotalSeconds < 30)
return "Just now";
if (timeSpan.TotalMinutes < 1)
{
time = timeSpan.Seconds;
unit = "second";
}
else if (timeSpan.TotalHours < 1)
{
time = timeSpan.Minutes;
unit = "minute";
}
else switch (timeSpan.TotalDays)
{
case < 1:
time = timeSpan.Hours;
unit = "hour";
break;
case < 7:
time = timeSpan.Days;
unit = "day";
break;
case < 30:
time = timeSpan.Days / 7;
unit = "week";
break;
case < 365:
time = timeSpan.Days / 30;
unit = "month";
break;
default:
time = timeSpan.Days / 365;
unit = "year";
break;
}
return $"{time} {unit}{(time > 1 ? "s" : string.Empty)} ago";
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@ -68,6 +68,8 @@
<Geometry x:Key="UnfoldIcon">M12 5.83l2.46 2.46c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L12.7 3.7c-.39-.39-1.02-.39-1.41 0L8.12 6.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 5.83zm0 12.34l-2.46-2.46c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l3.17 3.18c.39.39 1.02.39 1.41 0l3.17-3.17c.39-.39.39-1.02 0-1.41-.39-.39-1.02-.39-1.41 0L12 18.17z</Geometry>
<Geometry x:Key="LocateMeIcon">M11.71,17.99C8.53,17.84,6,15.22,6,12c0-3.31,2.69-6,6-6c3.22,0,5.84,2.53,5.99,5.71l-2.1-0.63C15.48,9.31,13.89,8,12,8 c-2.21,0-4,1.79-4,4c0,1.89,1.31,3.48,3.08,3.89L11.71,17.99z M22,12c0,0.3-0.01,0.6-0.04,0.9l-1.97-0.59C20,12.21,20,12.1,20,12 c0-4.42-3.58-8-8-8s-8,3.58-8,8s3.58,8,8,8c0.1,0,0.21,0,0.31-0.01l0.59,1.97C12.6,21.99,12.3,22,12,22C6.48,22,2,17.52,2,12 C2,6.48,6.48,2,12,2S22,6.48,22,12z M18.23,16.26l2.27-0.76c0.46-0.15,0.45-0.81-0.01-0.95l-7.6-2.28 c-0.38-0.11-0.74,0.24-0.62,0.62l2.28,7.6c0.14,0.47,0.8,0.48,0.95,0.01l0.76-2.27l3.91,3.91c0.2,0.2,0.51,0.2,0.71,0l1.27-1.27 c0.2-0.2,0.2-0.51,0-0.71L18.23,16.26z</Geometry>
<Geometry x:Key="MeshIcon">M1.8 6q-.525 0-.887-.35Q.55 5.3.55 4.8V4q0-1.425 1.012-2.438Q2.575.55 4 .55h.8q.5 0 .85.362.35.363.35.888 0 .5-.35.85T4.8 3H4q-.425 0-.712.287Q3 3.575 3 4v.8q0 .5-.35.85T1.8 6ZM4 23.45q-1.425 0-2.438-1.012Q.55 21.425.55 20v-.8q0-.5.363-.85.362-.35.887-.35.5 0 .85.35t.35.85v.8q0 .425.288.712Q3.575 21 4 21h.8q.5 0 .85.35t.35.85q0 .525-.35.887-.35.363-.85.363Zm15.2 0q-.5 0-.85-.363-.35-.362-.35-.887 0-.5.35-.85t.85-.35h.8q.425 0 .712-.288Q21 20.425 21 20v-.8q0-.5.35-.85t.85-.35q.525 0 .888.35.362.35.362.85v.8q0 1.425-1.012 2.438Q21.425 23.45 20 23.45ZM22.2 6q-.5 0-.85-.35T21 4.8V4q0-.425-.288-.713Q20.425 3 20 3h-.8q-.5 0-.85-.35T18 1.8q0-.525.35-.888.35-.362.85-.362h.8q1.425 0 2.438 1.012Q23.45 2.575 23.45 4v.8q0 .5-.362.85-.363.35-.888.35ZM12 17.35l1-.575v-4.1l3.55-2.075V9.425l-1-.575L12 10.925 8.45 8.85l-1 .575V10.6L11 12.675v4.1Zm-1.325 2.325-4.55-2.65q-.625-.35-.975-.963-.35-.612-.35-1.337V9.45q0-.725.35-1.337.35-.613.975-.963l4.55-2.65Q11.3 4.15 12 4.15t1.325.35l4.55 2.65q.625.35.975.963.35.612.35 1.337v5.275q0 .725-.35 1.337-.35.613-.975.963l-4.55 2.65q-.625.35-1.325.35t-1.325-.35Z</Geometry>
<Geometry x:Key="ArchiveIcon">M3.5 1.75v11.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 2 13.25V1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.185 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 12.25 15h-.5a.75.75 0 0 1 0-1.5h.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177L9.513 1.573a.25.25 0 0 0-.177-.073H7.25a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5h-3a.25.25 0 0 0-.25.25Zm3.75 8.75h.5c.966 0 1.75.784 1.75 1.75v3a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1-.75-.75v-3c0-.966.784-1.75 1.75-1.75ZM6 5.25a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 6 5.25Zm.75 2.25h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 6.75A.75.75 0 0 1 8.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 6.75ZM8.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 9.75A.75.75 0 0 1 8.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 9.75Zm-1 2.5v2.25h1v-2.25a.25.25 0 0 0-.25-.25h-.5a.25.25 0 0 0-.25.25Z</Geometry>
<Geometry x:Key="GitHubIcon">M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z</Geometry>
<Style x:Key="TabItemFillSpace" TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="Width">

View File

@ -0,0 +1,114 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.UpdateView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FModel"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="CanMinimize" Loaded="OnLoaded"
MinHeight="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.40'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}" >
<Setter Property="Title" Value="Releases" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Resources.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" Margin="10">
All releases listed below are available for download. They are sorted by date, with the latest release at the top.
We regularly remove old ones to keep the list clean and up to date with the latest UE releases.
If you wish to manually check for updates, this window is accessible via the Help > Releases menu.
</TextBlock>
<Grid Grid.Row="1" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- <Grid.Style> -->
<!-- <Style TargetType="Grid"> -->
<!-- <Setter Property="Visibility" Value="Visible" /> -->
<!-- <Style.Triggers> -->
<!-- <DataTrigger Binding="{Binding Title, RelativeSource={RelativeSource AncestorType=adonisControls:AdonisWindow}}" Value="Releases"> -->
<!-- <Setter Property="Visibility" Value="Collapsed" /> -->
<!-- </DataTrigger> -->
<!-- </Style.Triggers> -->
<!-- </Style> -->
<!-- </Grid.Style> -->
<Button Grid.Column="0" Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}"
VerticalAlignment="Top" Height="{Binding ActualHeight, ElementName=RemindButton}"
Click="OnDownloadLatest">
Download Latest Release
</Button>
<StackPanel Grid.Column="2">
<adonisControls:SplitButton x:Name="RemindButton" Content="Remind Me Now ..." Command="{Binding RemindMeCommand}">
<adonisControls:SplitButton.SplitMenu>
<ContextMenu>
<MenuItem Header="In 3 Days" Command="{Binding RemindMeCommand}" CommandParameter="Days" />
<MenuItem Header="Next Week" Command="{Binding RemindMeCommand}" CommandParameter="Week" />
<MenuItem Header="Next Month" Command="{Binding RemindMeCommand}" CommandParameter="Month" />
<MenuItem Header="Never" Command="{Binding RemindMeCommand}" CommandParameter="Never" />
</ContextMenu>
</adonisControls:SplitButton.SplitMenu>
</adonisControls:SplitButton>
<TextBlock VerticalAlignment="Bottom" HorizontalAlignment="Right" FontSize="10" Margin="0 2.5 0 0"
Text="{Binding NextUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, StringFormat=Next Refresh: {0:MMM d, yyyy}}" />
</StackPanel>
</Grid>
<Separator Grid.Row="2" Style="{StaticResource CustomSeparator}" Tag="History" />
<ScrollViewer Grid.Row="3" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding CommitsView}">
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<GroupBox adonisExtensions:LayerExtension.Layer="3"
Header="{Binding Name}"
HeaderStringFormat="Commits on {0:MMM d, yyyy}"
Margin="0 0 0 5">
<ItemsPresenter />
</GroupBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
<GroupStyle.Panel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:CommitControl Margin="0 0 0 1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -0,0 +1,27 @@
using System.Windows;
using FModel.ViewModels;
using FModel.Views.Resources.Controls;
namespace FModel.Views;
public partial class UpdateView
{
public UpdateView()
{
DataContext = new UpdateViewModel();
InitializeComponent();
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
if (DataContext is not UpdateViewModel viewModel) return;
await viewModel.Load();
}
private void OnDownloadLatest(object sender, RoutedEventArgs e)
{
if (DataContext is not UpdateViewModel viewModel) return;
viewModel.DownloadLatest();
}
}