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:
parent
5cbdfbc7a4
commit
531da8a1c0
9 changed files with 113 additions and 43 deletions
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
38
src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
Normal file
38
src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,9 @@ 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
|
||||||
{
|
{
|
||||||
|
@ -24,13 +26,28 @@ namespace Ryujinx.Ui.App.Common
|
||||||
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);
|
||||||
|
|
|
@ -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 _))
|
// Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
|
||||||
{
|
return;
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)");
|
}
|
||||||
|
|
||||||
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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue