Changed LastPlayed field from string to nullable DateTime (#4861)

* Changed LastPlayed field from string to nullable DateTime

Added ApplicationData.LastPlayedString property
Added NullableDateTimeConverter for the DateTime->string conversion in Avalonia

* Added migration from string-based last_played to DateTime-based last_played_utc

* Updated comment style

* Added MarkupExtension to NullableDateTimeConverter and changed its usage

Cleaned up leftover usings

* Missed one comment
This commit is contained in:
SamusAranX 2023-05-12 01:56:37 +02:00 committed by GitHub
parent 5cbdfbc7a4
commit 531da8a1c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 113 additions and 43 deletions

View file

@ -671,7 +671,7 @@ namespace Ryujinx.Ava
_viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => _viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{ {
appMetadata.LastPlayed = DateTime.UtcNow.ToString(); appMetadata.LastPlayed = DateTime.UtcNow;
}); });
return true; return true;

View file

@ -129,7 +129,7 @@
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding LastPlayed}" Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}"
TextAlignment="Right" TextAlignment="Right"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock

View file

@ -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;
}
}
}

View file

@ -1,4 +1,3 @@
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ui.App.Common; using Ryujinx.Ui.App.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -14,20 +13,20 @@ namespace Ryujinx.Ava.UI.Models.Generic
public int Compare(ApplicationData x, ApplicationData y) public int Compare(ApplicationData x, ApplicationData y)
{ {
string aValue = x.LastPlayed; var aValue = x.LastPlayed;
string bValue = y.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);
} }
} }
} }

View file

@ -1524,10 +1524,9 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => 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); appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
} }
}); });

View file

@ -10,27 +10,44 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Text.Json.Serialization;
namespace Ryujinx.Ui.App.Common namespace Ryujinx.Ui.App.Common
{ {
public class ApplicationData public class ApplicationData
{ {
public bool Favorite { get; set; } public bool Favorite { get; set; }
public byte[] Icon { get; set; } public byte[] Icon { get; set; }
public string TitleName { get; set; } public string TitleName { get; set; }
public string TitleId { get; set; } public string TitleId { get; set; }
public string Developer { get; set; } public string Developer { get; set; }
public string Version { get; set; } public string Version { get; set; }
public string TimePlayed { get; set; } public string TimePlayed { get; set; }
public double TimePlayedNum { get; set; } public double TimePlayedNum { get; set; }
public string LastPlayed { get; set; } public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; } public string FileExtension { get; set; }
public string FileSize { get; set; } public string FileSize { get; set; }
public double FileSizeBytes { get; set; } public double FileSizeBytes { get; set; }
public string Path { get; set; } public string Path { get; set; }
public BlitStruct<ApplicationControlProperty> ControlHolder { 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) public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
{ {
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read); using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);

View file

@ -414,21 +414,28 @@ namespace Ryujinx.Ui.App.Common
ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata => ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
{ {
appMetadata.Title = titleName; appMetadata.Title = titleName;
});
if (appMetadata.LastPlayed != "Never") if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
{
if (!DateTime.TryParse(appMetadata.LastPlayed, out _))
{ {
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 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() ApplicationData data = new()
{ {

View file

@ -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 class ApplicationMetadata
{ {
public string Title { get; set; } public string Title { get; set; }
public bool Favorite { get; set; } public bool Favorite { get; set; }
public double TimePlayed { 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; }
} }
} }

View file

@ -876,7 +876,7 @@ namespace Ryujinx.Ui
_applicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata => _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 => _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{ {
DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed); 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); appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
}
}); });
} }
} }
@ -1089,7 +1090,7 @@ namespace Ryujinx.Ui
args.AppData.Developer, args.AppData.Developer,
args.AppData.Version, args.AppData.Version,
args.AppData.TimePlayed, args.AppData.TimePlayed,
args.AppData.LastPlayed, args.AppData.LastPlayedString,
args.AppData.FileExtension, args.AppData.FileExtension,
args.AppData.FileSize, args.AppData.FileSize,
args.AppData.Path, args.AppData.Path,