diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 0efe2b16..4781e951 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -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); diff --git a/FModel/Constants.cs b/FModel/Constants.cs index a0bf4597..48507954 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -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"; diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index e46eac51..57d1b200 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -147,11 +147,11 @@ - + - + @@ -797,13 +797,17 @@ + + + + diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index f5f703a1..e11863fe 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -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]; /// /// TO DELETEEEEEEEEEEEEE diff --git a/FModel/ViewModels/ApiEndpointViewModel.cs b/FModel/ViewModels/ApiEndpointViewModel.cs index 47146eae..6276567a 100644 --- a/FModel/ViewModels/ApiEndpointViewModel.cs +++ b/FModel/ViewModels/ApiEndpointViewModel.cs @@ -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); } diff --git a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs index 1fbe18f0..04691521 100644 --- a/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs +++ b/FModel/ViewModels/ApiEndpoints/FModelApiEndpoint.cs @@ -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(message, () => new UpdateView { Title = message, ResizeMode = ResizeMode.NoResize }.ShowDialog()); } else { diff --git a/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs b/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs new file mode 100644 index 00000000..e6b2761d --- /dev/null +++ b/FModel/ViewModels/ApiEndpoints/GitHubApiEndpoint.cs @@ -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 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(request).ConfigureAwait(false); + return response.Data; + } + + public async Task GetReleaseAsync(string tag) + { + var request = new FRestRequest($"{Constants.GH_RELEASES}/tags/{tag}"); + var response = await _client.ExecuteAsync(request).ConfigureAwait(false); + return response.Data; + } +} diff --git a/FModel/ViewModels/ApiEndpoints/Models/GitHubResponse.cs b/FModel/ViewModels/ApiEndpoints/Models/GitHubResponse.cs new file mode 100644 index 00000000..c2c15d5d --- /dev/null +++ b/FModel/ViewModels/ApiEndpoints/Models/GitHubResponse.cs @@ -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; } +} diff --git a/FModel/ViewModels/ApplicationViewModel.cs b/FModel/ViewModels/ApplicationViewModel.cs index 1d314969..a67df7ad 100644 --- a/FModel/ViewModels/ApplicationViewModel.cs +++ b/FModel/ViewModels/ApplicationViewModel.cs @@ -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, diff --git a/FModel/ViewModels/Commands/MenuCommand.cs b/FModel/ViewModels/Commands/MenuCommand.cs index 7a510915..14b54904 100644 --- a/FModel/ViewModels/Commands/MenuCommand.cs +++ b/FModel/ViewModels/Commands/MenuCommand.cs @@ -54,9 +54,8 @@ public class MenuCommand : ViewModelCommand 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("Releases", () => new UpdateView().Show()); break; case "Help_BugsReport": Process.Start(new ProcessStartInfo { FileName = Constants.ISSUE_LINK, UseShellExecute = true }); diff --git a/FModel/ViewModels/Commands/RemindMeCommand.cs b/FModel/ViewModels/Commands/RemindMeCommand.cs new file mode 100644 index 00000000..7c78a952 --- /dev/null +++ b/FModel/ViewModels/Commands/RemindMeCommand.cs @@ -0,0 +1,40 @@ +using System; +using FModel.Framework; +using FModel.Settings; + +namespace FModel.ViewModels.Commands; + +public class RemindMeCommand : ViewModelCommand +{ + 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; + } + } +} diff --git a/FModel/ViewModels/UpdateViewModel.cs b/FModel/ViewModels/UpdateViewModel.cs new file mode 100644 index 00000000..55d85209 --- /dev/null +++ b/FModel/ViewModels/UpdateViewModel.cs @@ -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 Commits { get; } + public ICollectionView CommitsView { get; } + + public UpdateViewModel() + { + Commits = new RangeObservableCollection(); + 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(); + } +} diff --git a/FModel/Views/Resources/Controls/CommitControl.xaml b/FModel/Views/Resources/Controls/CommitControl.xaml new file mode 100644 index 00000000..00d331e4 --- /dev/null +++ b/FModel/Views/Resources/Controls/CommitControl.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/Views/Resources/Controls/CommitControl.xaml.cs b/FModel/Views/Resources/Controls/CommitControl.xaml.cs new file mode 100644 index 00000000..198c05ae --- /dev/null +++ b/FModel/Views/Resources/Controls/CommitControl.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace FModel.Views.Resources.Controls; + +public partial class CommitControl : UserControl +{ + public CommitControl() + { + InitializeComponent(); + } +} + diff --git a/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml b/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml new file mode 100644 index 00000000..256ce0a4 --- /dev/null +++ b/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml.cs b/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml.cs new file mode 100644 index 00000000..b779543a --- /dev/null +++ b/FModel/Views/Resources/Controls/CommitDownloaderControl.xaml.cs @@ -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(); + } +} + diff --git a/FModel/Views/Resources/Converters/CommitMessageConverter.cs b/FModel/Views/Resources/Converters/CommitMessageConverter.cs new file mode 100644 index 00000000..22b32fd3 --- /dev/null +++ b/FModel/Views/Resources/Converters/CommitMessageConverter.cs @@ -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(); + } +} diff --git a/FModel/Views/Resources/Converters/DateTimeToDateConverter.cs b/FModel/Views/Resources/Converters/DateTimeToDateConverter.cs new file mode 100644 index 00000000..1c5bea71 --- /dev/null +++ b/FModel/Views/Resources/Converters/DateTimeToDateConverter.cs @@ -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(); + } +} diff --git a/FModel/Views/Resources/Converters/InvertBooleanConverter.cs b/FModel/Views/Resources/Converters/InvertBooleanConverter.cs new file mode 100644 index 00000000..90b9c264 --- /dev/null +++ b/FModel/Views/Resources/Converters/InvertBooleanConverter.cs @@ -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(); + } +} diff --git a/FModel/Views/Resources/Converters/RelativeDateTimeConverter.cs b/FModel/Views/Resources/Converters/RelativeDateTimeConverter.cs new file mode 100644 index 00000000..ac9aaa34 --- /dev/null +++ b/FModel/Views/Resources/Converters/RelativeDateTimeConverter.cs @@ -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(); + } +} diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index aa279955..03e061d9 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -68,6 +68,8 @@ 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 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 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 + 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 + 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 + + + + + + + + + + + + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/Views/UpdateView.xaml.cs b/FModel/Views/UpdateView.xaml.cs new file mode 100644 index 00000000..3b9ca455 --- /dev/null +++ b/FModel/Views/UpdateView.xaml.cs @@ -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(); + } +} +