diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs index 0955fb270f..795c3f7a70 100644 --- a/src/Ryujinx.Ava/AppHost.cs +++ b/src/Ryujinx.Ava/AppHost.cs @@ -671,7 +671,7 @@ namespace Ryujinx.Ava _viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow.ToString(); + appMetadata.LastPlayed = DateTime.UtcNow; }); return true; diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml index fa8ebf627b..227b4723bd 100644 --- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml @@ -129,7 +129,7 @@ TextWrapping="Wrap" /> <TextBlock HorizontalAlignment="Stretch" - Text="{Binding LastPlayed}" + Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}" TextAlignment="Right" TextWrapping="Wrap" /> <TextBlock diff --git a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs new file mode 100644 index 0000000000..1d862de015 --- /dev/null +++ b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs @@ -0,0 +1,38 @@ +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; +using Ryujinx.Ava.Common.Locale; +using System; +using System.Globalization; + +namespace Ryujinx.Ava.UI.Helpers +{ + internal class NullableDateTimeConverter : MarkupExtension, IValueConverter + { + private static readonly NullableDateTimeConverter _instance = new(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + { + return LocaleManager.Instance[LocaleKeys.Never]; + } + + if (value is DateTime dateTime) + { + return dateTime.ToLocalTime().ToString(culture); + } + + throw new NotSupportedException(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return _instance; + } + } +} \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs index 98caceb509..3627ada9a9 100644 --- a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs +++ b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs @@ -1,4 +1,3 @@ -using Ryujinx.Ava.Common.Locale; using Ryujinx.Ui.App.Common; using System; using System.Collections.Generic; @@ -14,20 +13,20 @@ namespace Ryujinx.Ava.UI.Models.Generic public int Compare(ApplicationData x, ApplicationData y) { - string aValue = x.LastPlayed; - string bValue = y.LastPlayed; + var aValue = x.LastPlayed; + var bValue = y.LastPlayed; - if (aValue == LocaleManager.Instance[LocaleKeys.Never]) + if (!aValue.HasValue) { - aValue = DateTime.UnixEpoch.ToString(); + aValue = DateTime.UnixEpoch; } - if (bValue == LocaleManager.Instance[LocaleKeys.Never]) + if (!bValue.HasValue) { - bValue = DateTime.UnixEpoch.ToString(); + bValue = DateTime.UnixEpoch; } - return (IsAscending ? 1 : -1) * DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue)); + return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value); } } } \ No newline at end of file diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs index 4db78afeb0..f8dd414358 100644 --- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs @@ -1524,10 +1524,9 @@ namespace Ryujinx.Ava.UI.ViewModels { ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime)) + if (appMetadata.LastPlayed.HasValue) { - double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; - + double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); } }); diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs index d9d3cf6853..f0aa40be26 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs @@ -10,27 +10,44 @@ using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Logging; using Ryujinx.HLE.FileSystem; using System; +using System.Globalization; using System.IO; +using System.Text.Json.Serialization; namespace Ryujinx.Ui.App.Common { public class ApplicationData { - public bool Favorite { get; set; } - public byte[] Icon { get; set; } - public string TitleName { get; set; } - public string TitleId { get; set; } - public string Developer { get; set; } - public string Version { get; set; } - public string TimePlayed { get; set; } - public double TimePlayedNum { get; set; } - public string LastPlayed { get; set; } - public string FileExtension { get; set; } - public string FileSize { get; set; } - public double FileSizeBytes { get; set; } - public string Path { get; set; } + public bool Favorite { get; set; } + public byte[] Icon { get; set; } + public string TitleName { get; set; } + public string TitleId { get; set; } + public string Developer { get; set; } + public string Version { get; set; } + public string TimePlayed { get; set; } + public double TimePlayedNum { get; set; } + public DateTime? LastPlayed { get; set; } + public string FileExtension { get; set; } + public string FileSize { get; set; } + public double FileSizeBytes { get; set; } + public string Path { get; set; } public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; } - + + [JsonIgnore] + public string LastPlayedString + { + get + { + if (!LastPlayed.HasValue) + { + // TODO: maybe put localized string here instead of just "Never" + return "Never"; + } + + return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture); + } + } + public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath) { using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs index b7b57f1a29..0407036a02 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs @@ -414,21 +414,28 @@ namespace Ryujinx.Ui.App.Common ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => { appMetadata.Title = titleName; - }); - if (appMetadata.LastPlayed != "Never") - { - if (!DateTime.TryParse(appMetadata.LastPlayed, out _)) + if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue) { - Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)"); + // Don't do the migration if last_played doesn't exist or last_played_utc already has a value. + return; + } - appMetadata.LastPlayed = "Never"; + // Migrate from string-based last_played to DateTime-based last_played_utc. + if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed)) + { + Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc"); + appMetadata.LastPlayed = lastPlayedOldParsed; + + // Migration successful: deleting last_played from the metadata file. + appMetadata.LastPlayedOld = default; } else { - appMetadata.LastPlayed = appMetadata.LastPlayed[..^3]; + // Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it. + Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)"); } - } + }); ApplicationData data = new() { diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs index e19f7483fc..0abd4680da 100644 --- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs +++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs @@ -1,10 +1,19 @@ -namespace Ryujinx.Ui.App.Common +using System; +using System.Text.Json.Serialization; + +namespace Ryujinx.Ui.App.Common { public class ApplicationMetadata { public string Title { get; set; } public bool Favorite { get; set; } public double TimePlayed { get; set; } - public string LastPlayed { get; set; } = "Never"; + + [JsonPropertyName("last_played_utc")] + public DateTime? LastPlayed { get; set; } = null; + + [JsonPropertyName("last_played")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string LastPlayedOld { get; set; } } } \ No newline at end of file diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs index f4cb3d0727..7cae62227d 100644 --- a/src/Ryujinx/Ui/MainWindow.cs +++ b/src/Ryujinx/Ui/MainWindow.cs @@ -876,7 +876,7 @@ namespace Ryujinx.Ui _applicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => { - appMetadata.LastPlayed = DateTime.UtcNow.ToString(); + appMetadata.LastPlayed = DateTime.UtcNow; }); } } @@ -1019,10 +1019,11 @@ namespace Ryujinx.Ui { _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { - DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed); - double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; - - appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); + if (appMetadata.LastPlayed.HasValue) + { + double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds; + appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero); + } }); } } @@ -1089,7 +1090,7 @@ namespace Ryujinx.Ui args.AppData.Developer, args.AppData.Version, args.AppData.TimePlayed, - args.AppData.LastPlayed, + args.AppData.LastPlayedString, args.AppData.FileExtension, args.AppData.FileSize, args.AppData.Path,