diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index cd066efb..4d751e2a 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -716,7 +716,7 @@ namespace Ryujinx.Ava
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePreGame();
});
return true;
diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs
index 168e9216..cc062a25 100644
--- a/src/Ryujinx.Ava/Program.cs
+++ b/src/Ryujinx.Ava/Program.cs
@@ -6,13 +6,13 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Common.SystemInfo;
using System;
using System.IO;
using System.Runtime.InteropServices;
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
index 09011005..9004f751 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationListView.axaml
@@ -126,17 +126,17 @@
Spacing="5">
diff --git a/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
new file mode 100644
index 00000000..73789698
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
@@ -0,0 +1,43 @@
+using Avalonia.Data.Converters;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.Ui.Common.Helper;
+using System;
+using System.Globalization;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ ///
+ /// This makes sure that the string "Never" that's returned by is properly localized in the Avalonia UI.
+ /// After the Avalonia UI has been made the default and the GTK UI is removed, should be updated to directly return a localized string.
+ ///
+ internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
+ {
+ private static readonly LocalizedNeverConverter _instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is not string valStr)
+ {
+ return "";
+ }
+
+ if (valStr == "Never")
+ {
+ return LocaleManager.Instance[LocaleKeys.Never];
+ }
+
+ return valStr;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return _instance;
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs b/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
deleted file mode 100644
index e9193761..00000000
--- a/src/Ryujinx.Ava/UI/Helpers/NullableDateTimeConverter.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-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;
- }
- }
-}
diff --git a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
index 8a434655..8340d39d 100644
--- a/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
+++ b/src/Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
public int Compare(ApplicationData x, ApplicationData y)
{
- var aValue = x.LastPlayed;
- var bValue = y.LastPlayed;
+ DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
- if (!aValue.HasValue)
+ if (x?.LastPlayed != null)
{
- aValue = DateTime.UnixEpoch;
+ aValue = x.LastPlayed.Value;
}
- if (!bValue.HasValue)
+ if (y?.LastPlayed != null)
{
- bValue = DateTime.UnixEpoch;
+ bValue = y.LastPlayed.Value;
}
- return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value);
+ return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue);
}
}
}
diff --git a/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
new file mode 100644
index 00000000..d53ff566
--- /dev/null
+++ b/src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
@@ -0,0 +1,31 @@
+using Ryujinx.Ui.App.Common;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.UI.Models.Generic
+{
+ internal class TimePlayedSortComparer : IComparer
+ {
+ public TimePlayedSortComparer() { }
+ public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
+
+ public bool IsAscending { get; }
+
+ public int Compare(ApplicationData x, ApplicationData y)
+ {
+ TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero;
+
+ if (x?.TimePlayed != null)
+ {
+ aValue = x.TimePlayed;
+ }
+
+ if (y?.TimePlayed != null)
+ {
+ bValue = y.TimePlayed;
+ }
+
+ return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue);
+ }
+ }
+}
diff --git a/src/Ryujinx.Ava/UI/Models/SaveModel.cs b/src/Ryujinx.Ava/UI/Models/SaveModel.cs
index f15befbb..7b476932 100644
--- a/src/Ryujinx.Ava/UI/Models/SaveModel.cs
+++ b/src/Ryujinx.Ava/UI/Models/SaveModel.cs
@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.App.Common;
-using System;
+using Ryujinx.Ui.Common.Helper;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
public bool SizeAvailable { get; set; }
- public string SizeString => GetSizeString();
-
- private string GetSizeString()
- {
- const int Scale = 1024;
- string[] orders = { "GiB", "MiB", "KiB" };
- long max = (long)Math.Pow(Scale, orders.Length);
-
- foreach (string order in orders)
- {
- if (Size > max)
- {
- return $"{decimal.Divide(Size, max):##.##} {order}";
- }
-
- max /= Scale;
- }
-
- return "0 KiB";
- }
+ public string SizeString => ValueFormatUtils.FormatFileSize(Size);
public SaveModel(SaveDataInfo info)
{
diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index b1490520..80df5d39 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -930,21 +930,20 @@ namespace Ryujinx.Ava.UI.ViewModels
return SortMode switch
{
#pragma warning disable IDE0055 // Disable formatting
+ ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName)
+ : SortExpressionComparer.Descending(app => app.TitleName),
+ ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer)
+ : SortExpressionComparer.Descending(app => app.Developer),
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
- ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSizeBytes)
- : SortExpressionComparer.Descending(app => app.FileSizeBytes),
- ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer.Ascending(app => app.TimePlayedNum)
- : SortExpressionComparer.Descending(app => app.TimePlayedNum),
- ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName)
- : SortExpressionComparer.Descending(app => app.TitleName),
+ ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
+ ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension)
+ : SortExpressionComparer.Descending(app => app.FileExtension),
+ ApplicationSort.FileSize => IsAscending ? SortExpressionComparer.Ascending(app => app.FileSize)
+ : SortExpressionComparer.Descending(app => app.FileSize),
+ ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path)
+ : SortExpressionComparer.Descending(app => app.Path),
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer.Ascending(app => app.Favorite)
: SortExpressionComparer.Descending(app => app.Favorite),
- ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer)
- : SortExpressionComparer.Descending(app => app.Developer),
- ApplicationSort.FileType => IsAscending ? SortExpressionComparer.Ascending(app => app.FileExtension)
- : SortExpressionComparer.Descending(app => app.FileExtension),
- ApplicationSort.Path => IsAscending ? SortExpressionComparer.Ascending(app => app.Path)
- : SortExpressionComparer.Descending(app => app.Path),
_ => null,
#pragma warning restore IDE0055
};
@@ -1549,13 +1548,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{
- if (appMetadata.LastPlayed.HasValue)
- {
- double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
- appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
- }
-
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePostGame();
});
}
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
index 1be883ee..65ab01ee 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
@@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.Ui.Common.Helper;
using System;
-using System.Globalization;
using System.IO;
-using System.Text.Json.Serialization;
namespace Ryujinx.Ui.App.Common
{
@@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common
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 TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
- public string FileSize { get; set; }
- public double FileSizeBytes { get; set; }
+ public long FileSize { get; set; }
public string Path { get; set; }
public BlitStruct ControlHolder { get; set; }
- [JsonIgnore]
- public string LastPlayedString
- {
- get
- {
- if (!LastPlayed.HasValue)
- {
- // TODO: maybe put localized string here instead of just "Never"
- return "Never";
- }
+ public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
- return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
- }
- }
+ public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
+
+ public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
{
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 2f688126..46f29851 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common
return;
}
- double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
+ long fileSize = new FileInfo(applicationPath).Length;
string titleName = "Unknown";
string titleId = "0000000000000000";
string developer = "Unknown";
@@ -425,25 +425,25 @@ namespace Ryujinx.Ui.App.Common
{
appMetadata.Title = titleName;
- if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
+ // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
+ if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
{
- // Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
- return;
+ appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
+ appMetadata.TimePlayedOld = default;
}
- // Migrate from string-based last_played to DateTime-based last_played_utc.
- if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
+ if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
{
- Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc");
- appMetadata.LastPlayed = lastPlayedOldParsed;
+ // Migrate from string-based last_played to DateTime-based last_played_utc.
+ if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ {
+ appMetadata.LastPlayed = lastPlayedOldParsed;
+
+ // Migration successful: deleting last_played from the metadata file.
+ appMetadata.LastPlayedOld = default;
+ }
- // Migration successful: deleting last_played from the metadata file.
- appMetadata.LastPlayedOld = default;
- }
- else
- {
- // 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?)");
}
});
@@ -455,12 +455,10 @@ namespace Ryujinx.Ui.App.Common
TitleId = titleId,
Developer = developer,
Version = version,
- TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
- TimePlayedNum = appMetadata.TimePlayed,
+ TimePlayed = appMetadata.TimePlayed,
LastPlayed = appMetadata.LastPlayed,
- FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
- FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
- FileSizeBytes = fileSize,
+ FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
+ FileSize = fileSize,
Path = applicationPath,
ControlHolder = controlHolder,
};
@@ -716,31 +714,6 @@ namespace Ryujinx.Ui.App.Common
return applicationIcon ?? _ncaIcon;
}
- private static string ConvertSecondsToFormattedString(double seconds)
- {
- TimeSpan time = TimeSpan.FromSeconds(seconds);
-
- string timeString;
- if (time.Days != 0)
- {
- timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
- }
- else if (time.Hours != 0)
- {
- timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
- }
- else if (time.Minutes != 0)
- {
- timeString = $"{time.Minutes:D2}m";
- }
- else
- {
- timeString = "Never";
- }
-
- return timeString;
- }
-
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
{
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
index 01b857a6..9e2ca687 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationMetadata.cs
@@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
{
public string Title { get; set; }
public bool Favorite { get; set; }
- public double TimePlayed { get; set; }
+
+ [JsonPropertyName("timespan_played")]
+ public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
[JsonPropertyName("last_played_utc")]
public DateTime? LastPlayed { get; set; } = null;
+ [JsonPropertyName("time_played")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public double TimePlayedOld { get; set; }
+
[JsonPropertyName("last_played")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string LastPlayedOld { get; set; }
+
+ ///
+ /// Updates . Call this before launching a game.
+ ///
+ public void UpdatePreGame()
+ {
+ LastPlayed = DateTime.UtcNow;
+ }
+
+ ///
+ /// Updates and . Call this after a game ends.
+ ///
+ public void UpdatePostGame()
+ {
+ DateTime? prevLastPlayed = LastPlayed;
+ UpdatePreGame();
+
+ if (!prevLastPlayed.HasValue)
+ {
+ return;
+ }
+
+ TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
+ double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
+ TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
+ }
}
}
diff --git a/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
new file mode 100644
index 00000000..951cd089
--- /dev/null
+++ b/src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace Ryujinx.Ui.Common.Helper
+{
+ public static class ValueFormatUtils
+ {
+ private static readonly string[] _fileSizeUnitStrings =
+ {
+ "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing
+ "KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values
+ };
+
+ ///
+ /// Used by .
+ ///
+ public enum FileSizeUnits
+ {
+ Auto = -1,
+ Bytes = 0,
+ Kibibytes = 1,
+ Mebibytes = 2,
+ Gibibytes = 3,
+ Tebibytes = 4,
+ Pebibytes = 5,
+ Exbibytes = 6,
+ Kilobytes = 7,
+ Megabytes = 8,
+ Gigabytes = 9,
+ Terabytes = 10,
+ Petabytes = 11,
+ Exabytes = 12,
+ }
+
+ private const double SizeBase10 = 1000;
+ private const double SizeBase2 = 1024;
+ private const int UnitEBIndex = 6;
+
+ #region Value formatters
+
+ ///
+ /// Creates a human-readable string from a .
+ ///
+ /// The to be formatted.
+ /// A formatted string that can be displayed in the UI.
+ public static string FormatTimeSpan(TimeSpan? timeSpan)
+ {
+ if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
+ {
+ // Game was never played
+ return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
+ }
+
+ if (timeSpan.Value.TotalDays < 1)
+ {
+ // Game was played for less than a day
+ return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
+ }
+
+ // Game was played for more than a day
+ TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
+ string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
+
+ return $"{timeSpan.Value.Days}d, {onlyTimeString}";
+ }
+
+ ///
+ /// Creates a human-readable string from a .
+ ///
+ /// The to be formatted. This is expected to be UTC-based.
+ /// The that's used in formatting. Defaults to .
+ /// A formatted string that can be displayed in the UI.
+ public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
+ {
+ culture ??= CultureInfo.CurrentCulture;
+
+ if (!utcDateTime.HasValue)
+ {
+ // In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
+ return "Never";
+ }
+
+ return utcDateTime.Value.ToLocalTime().ToString(culture);
+ }
+
+ ///
+ /// Creates a human-readable file size string.
+ ///
+ /// The file size in bytes.
+ /// Formats the passed size value as this unit, bypassing the automatic unit choice.
+ /// A human-readable file size string.
+ public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
+ {
+ if (size <= 0)
+ {
+ return $"0 {_fileSizeUnitStrings[0]}";
+ }
+
+ int unitIndex = (int)forceUnit;
+ if (forceUnit == FileSizeUnits.Auto)
+ {
+ unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
+
+ // Apply an upper bound so that exabytes are the biggest unit used when formatting.
+ if (unitIndex > UnitEBIndex)
+ {
+ unitIndex = UnitEBIndex;
+ }
+ }
+
+ double sizeRounded;
+
+ if (unitIndex > UnitEBIndex)
+ {
+ sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
+ }
+ else
+ {
+ sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
+ }
+
+ string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
+
+ return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
+ }
+
+ #endregion
+
+ #region Value parsers
+
+ ///
+ /// Parses a string generated by and returns the original .
+ ///
+ /// A string representing a .
+ /// A object. If the input string couldn't been parsed, is returned.
+ public static TimeSpan ParseTimeSpan(string timeSpanString)
+ {
+ TimeSpan returnTimeSpan = TimeSpan.Zero;
+
+ // An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day.
+ // Here, we split the input string to check if it's the former or the latter.
+ var valueSplit = timeSpanString.Split(", ");
+ if (valueSplit.Length > 1)
+ {
+ var dayPart = valueSplit[0].Split("d")[0];
+ if (int.TryParse(dayPart, out int days))
+ {
+ returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
+ }
+ }
+
+ if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
+ {
+ returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
+ }
+
+ return returnTimeSpan;
+ }
+
+ ///
+ /// Parses a string generated by and returns the original .
+ ///
+ /// The string representing a .
+ /// A object. If the input string couldn't be parsed, is returned.
+ public static DateTime ParseDateTime(string dateTimeString)
+ {
+ if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
+ {
+ // Games that were never played are supposed to appear before the oldest played games in the list,
+ // so returning DateTime.UnixEpoch here makes sense.
+ return DateTime.UnixEpoch;
+ }
+
+ return parsedDateTime;
+ }
+
+ ///
+ /// Parses a string generated by and returns a representing a number of bytes.
+ ///
+ /// A string representing a file size formatted with .
+ /// A representing a number of bytes.
+ public static long ParseFileSize(string sizeString)
+ {
+ // Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
+ for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
+ {
+ string unit = _fileSizeUnitStrings[i];
+ if (!sizeString.EndsWith(unit))
+ {
+ continue;
+ }
+
+ string numberString = sizeString.Split(" ")[0];
+ if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
+ {
+ break;
+ }
+
+ double sizeBase = SizeBase2;
+
+ // If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value.
+ if (i > UnitEBIndex)
+ {
+ i -= UnitEBIndex;
+ sizeBase = SizeBase10;
+ }
+
+ number *= Math.Pow(sizeBase, i);
+
+ return Convert.ToInt64(number);
+ }
+
+ return 0;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
similarity index 98%
rename from src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
index 08aa452e..5f1ab541 100644
--- a/src/Ryujinx.Common/SystemInfo/LinuxSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
@@ -5,7 +5,7 @@ using System.Globalization;
using System.IO;
using System.Runtime.Versioning;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("linux")]
class LinuxSystemInfo : SystemInfo
diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
similarity index 99%
rename from src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
index a968ad17..3508ae3a 100644
--- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("macos")]
partial class MacOSSystemInfo : SystemInfo
diff --git a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
similarity index 87%
rename from src/Ryujinx.Common/SystemInfo/SystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
index 55ec0127..6a4fe680 100644
--- a/src/Ryujinx.Common/SystemInfo/SystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
@@ -1,10 +1,11 @@
using Ryujinx.Common.Logging;
+using Ryujinx.Ui.Common.Helper;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using System.Text;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
public class SystemInfo
{
@@ -20,13 +21,13 @@ namespace Ryujinx.Common.SystemInfo
CpuName = "Unknown";
}
- private static string ToMiBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : $"{bytesValue / 1024 / 1024} MiB";
+ private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes);
public void Print()
{
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
- Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToMiBString(RamTotal)} ; Available {ToMiBString(RamAvailable)}");
+ Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}");
}
public static SystemInfo Gather()
diff --git a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
similarity index 98%
rename from src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
rename to src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
index 3b36d6e2..9bb0fbf7 100644
--- a/src/Ryujinx.Common/SystemInfo/WindowsSystemInfo.cs
+++ b/src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
@@ -4,7 +4,7 @@ using System.Management;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-namespace Ryujinx.Common.SystemInfo
+namespace Ryujinx.Ui.Common.SystemInfo
{
[SupportedOSPlatform("windows")]
partial class WindowsSystemInfo : SystemInfo
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 50151d73..afb6a992 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -3,7 +3,6 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
-using Ryujinx.Common.SystemInfo;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
@@ -11,6 +10,7 @@ using Ryujinx.Ui;
using Ryujinx.Ui.Common;
using Ryujinx.Ui.Common.Configuration;
using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Common.SystemInfo;
using Ryujinx.Ui.Widgets;
using SixLabors.ImageSharp.Formats.Jpeg;
using System;
diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs
index 0c0eefd2..c7a72ab9 100644
--- a/src/Ryujinx/Ui/Helper/SortHelper.cs
+++ b/src/Ryujinx/Ui/Helper/SortHelper.cs
@@ -1,4 +1,5 @@
using Gtk;
+using Ryujinx.Ui.Common.Helper;
using System;
namespace Ryujinx.Ui.Helper
@@ -7,88 +8,26 @@ namespace Ryujinx.Ui.Helper
{
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
- static string ReverseFormat(string time)
- {
- if (time == "Never")
- {
- return "00";
- }
+ TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
+ TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
- var numbers = time.Split(new char[] { 'd', 'h', 'm' });
-
- time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
-
- if (numbers.Length == 2)
- {
- return $"00.00:{time}";
- }
- else if (numbers.Length == 3)
- {
- return $"00.{time}";
- }
-
- return time;
- }
-
- string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
- string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
-
- return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
+ return TimeSpan.Compare(aTimeSpan, bTimeSpan);
}
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
- string aValue = model.GetValue(a, 6).ToString();
- string bValue = model.GetValue(b, 6).ToString();
+ DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
+ DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
- if (aValue == "Never")
- {
- aValue = DateTime.UnixEpoch.ToString();
- }
-
- if (bValue == "Never")
- {
- bValue = DateTime.UnixEpoch.ToString();
- }
-
- return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
+ return DateTime.Compare(aDateTime, bDateTime);
}
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
{
- string aValue = model.GetValue(a, 8).ToString();
- string bValue = model.GetValue(b, 8).ToString();
+ long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
+ long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
- if (aValue[^3..] == "GiB")
- {
- aValue = (float.Parse(aValue[0..^3]) * 1024).ToString();
- }
- else
- {
- aValue = aValue[0..^3];
- }
-
- if (bValue[^3..] == "GiB")
- {
- bValue = (float.Parse(bValue[0..^3]) * 1024).ToString();
- }
- else
- {
- bValue = bValue[0..^3];
- }
-
- if (float.Parse(aValue) > float.Parse(bValue))
- {
- return -1;
- }
- else if (float.Parse(bValue) > float.Parse(aValue))
- {
- return 1;
- }
- else
- {
- return 0;
- }
+ return aSize.CompareTo(bSize);
}
}
}
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
index a9d4be10..8b0b35e6 100644
--- a/src/Ryujinx/Ui/MainWindow.cs
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -954,7 +954,7 @@ namespace Ryujinx.Ui
ApplicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
{
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePreGame();
});
}
}
@@ -1097,13 +1097,7 @@ namespace Ryujinx.Ui
{
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
{
- if (appMetadata.LastPlayed.HasValue)
- {
- double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
- appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
- }
-
- appMetadata.LastPlayed = DateTime.UtcNow;
+ appMetadata.UpdatePostGame();
});
}
}
@@ -1177,10 +1171,10 @@ namespace Ryujinx.Ui
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
- args.AppData.TimePlayed,
+ args.AppData.TimePlayedString,
args.AppData.LastPlayedString,
args.AppData.FileExtension,
- args.AppData.FileSize,
+ args.AppData.FileSizeString,
args.AppData.Path,
args.AppData.ControlHolder);
});