misc: move Models & Helpers into Common & Avalonia projects

This commit is contained in:
Evan Husted 2024-12-29 19:09:28 -06:00
parent 9baaa2b8f8
commit 6caab1aa37
65 changed files with 141 additions and 146 deletions

View file

@ -0,0 +1,112 @@
using Ryujinx.Common.Logging;
using System.Collections.Generic;
namespace Ryujinx.UI.Common.Helper
{
public static class CommandLineState
{
public static string[] Arguments { get; private set; }
public static bool? OverrideDockedMode { get; private set; }
public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static bool HideAvailableUpdates { get; private set; }
public static void ParseArguments(string[] args)
{
List<string> arguments = new();
// Parse Arguments.
for (int i = 0; i < args.Length; ++i)
{
string arg = args[i];
switch (arg)
{
case "-r":
case "--root-data-dir":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
BaseDirPathArg = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-p":
case "--profile":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
Profile = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-f":
case "--fullscreen":
StartFullscreenArg = true;
arguments.Add(arg);
break;
case "-g":
case "--graphics-backend":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideGraphicsBackend = args[++i];
break;
case "-i":
case "--application-id":
LaunchApplicationId = args[++i];
break;
case "--docked-mode":
OverrideDockedMode = true;
break;
case "--handheld-mode":
OverrideDockedMode = false;
break;
case "--hide-cursor":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideHideCursor = args[++i];
break;
case "--hide-updates":
HideAvailableUpdates = true;
break;
case "--software-gui":
OverrideHardwareAcceleration = false;
break;
default:
LaunchPathArg = arg;
break;
}
}
Arguments = arguments.ToArray();
}
}
}

View file

@ -0,0 +1,63 @@
using Ryujinx.Common.Logging;
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.UI.Common.Helper
{
public static partial class ConsoleHelper
{
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32")]
private static partial nint GetConsoleWindow();
[SupportedOSPlatform("windows")]
[LibraryImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
[SupportedOSPlatform("windows")]
[LibraryImport("user32")]
private static partial nint GetForegroundWindow();
[SupportedOSPlatform("windows")]
[LibraryImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetForegroundWindow(nint hWnd);
public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows();
public static void SetConsoleWindowState(bool show)
{
if (OperatingSystem.IsWindows())
{
SetConsoleWindowStateWindows(show);
}
else if (show == false)
{
Logger.Warning?.Print(LogClass.Application, "OS doesn't support hiding console window");
}
}
[SupportedOSPlatform("windows")]
private static void SetConsoleWindowStateWindows(bool show)
{
const int SW_HIDE = 0;
const int SW_SHOW = 5;
nint hWnd = GetConsoleWindow();
if (hWnd == nint.Zero)
{
Logger.Warning?.Print(LogClass.Application, "Attempted to show/hide console window but console window does not exist");
return;
}
SetForegroundWindow(hWnd);
hWnd = GetForegroundWindow();
ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE);
}
}
}

View file

@ -0,0 +1,198 @@
using Microsoft.Win32;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.UI.Common.Helper
{
public static partial class FileAssociationHelper
{
private static readonly string[] _fileExtensions = [".nca", ".nro", ".nso", ".nsp", ".xci"];
[SupportedOSPlatform("linux")]
private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
private const int SHCNE_ASSOCCHANGED = 0x8000000;
private const int SHCNF_FLUSH = 0x1000;
[LibraryImport("shell32.dll", SetLastError = true)]
public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2);
public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows());
public static bool AreMimeTypesRegistered
{
get
{
if (OperatingSystem.IsLinux())
{
return AreMimeTypesRegisteredLinux();
}
if (OperatingSystem.IsWindows())
{
return AreMimeTypesRegisteredWindows();
}
// TODO: Add macOS support.
return false;
}
}
[SupportedOSPlatform("linux")]
private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml"));
[SupportedOSPlatform("linux")]
private static bool InstallLinuxMimeTypes(bool uninstall = false)
{
string installKeyword = uninstall ? "uninstall" : "install";
if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux()))
{
string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml");
string additionalArgs = !uninstall ? "--novendor" : string.Empty;
using Process mimeProcess = new();
mimeProcess.StartInfo.FileName = "xdg-mime";
mimeProcess.StartInfo.Arguments = $"{installKeyword} {additionalArgs} --mode user {mimeTypesFile}";
mimeProcess.Start();
mimeProcess.WaitForExit();
if (mimeProcess.ExitCode != 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Unable to {installKeyword} mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
return false;
}
using Process updateMimeProcess = new();
updateMimeProcess.StartInfo.FileName = "update-mime-database";
updateMimeProcess.StartInfo.Arguments = _mimeDbPath;
updateMimeProcess.Start();
updateMimeProcess.WaitForExit();
if (updateMimeProcess.ExitCode != 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
}
}
return true;
}
[SupportedOSPlatform("windows")]
private static bool AreMimeTypesRegisteredWindows()
{
return _fileExtensions.Aggregate(false,
(current, ext) => current | CheckRegistering(ext)
);
static bool CheckRegistering(string ext)
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}");
var openCmd = key?.OpenSubKey(@"shell\open\command");
if (openCmd is null)
{
return false;
}
string keyValue = (string)openCmd.GetValue(string.Empty);
return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName));
}
}
[SupportedOSPlatform("windows")]
private static bool InstallWindowsMimeTypes(bool uninstall = false)
{
bool registered = _fileExtensions.Aggregate(false,
(current, ext) => current | RegisterExtension(ext, uninstall)
);
// Notify Explorer the file association has been changed.
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero);
return registered;
static bool RegisterExtension(string ext, bool uninstall = false)
{
string keyString = @$"Software\Classes\{ext}";
if (uninstall)
{
// If the types don't already exist, there's nothing to do, and we can call this operation successful.
if (!AreMimeTypesRegisteredWindows())
{
return true;
}
Logger.Debug?.Print(LogClass.Application, $"Removing type association {ext}");
Registry.CurrentUser.DeleteSubKeyTree(keyString);
Logger.Debug?.Print(LogClass.Application, $"Removed type association {ext}");
}
else
{
using var key = Registry.CurrentUser.CreateSubKey(keyString);
if (key is null)
{
return false;
}
Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}");
using var openCmd = key.CreateSubKey(@"shell\open\command");
openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\"");
Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}");
}
return true;
}
}
public static bool Install()
{
if (OperatingSystem.IsLinux())
{
return InstallLinuxMimeTypes();
}
if (OperatingSystem.IsWindows())
{
return InstallWindowsMimeTypes();
}
// TODO: Add macOS support.
return false;
}
public static bool Uninstall()
{
if (OperatingSystem.IsLinux())
{
return InstallLinuxMimeTypes(true);
}
if (OperatingSystem.IsWindows())
{
return InstallWindowsMimeTypes(true);
}
// TODO: Add macOS support.
return false;
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
namespace Ryujinx.UI.Common.Helper
{
[SupportedOSPlatform("linux")]
public static class LinuxHelper
{
// NOTE: This value was determined by manual tests and might need to be increased again.
public const int RecommendedVmMaxMapCount = 524288;
public const string VmMaxMapCountPath = "/proc/sys/vm/max_map_count";
public const string SysCtlConfigPath = "/etc/sysctl.d/99-Ryujinx.conf";
public static int VmMaxMapCount => int.Parse(File.ReadAllText(VmMaxMapCountPath));
public static string PkExecPath { get; } = GetBinaryPath("pkexec");
private static string GetBinaryPath(string binary)
{
string pathVar = Environment.GetEnvironmentVariable("PATH");
if (pathVar is null || string.IsNullOrEmpty(binary))
{
return null;
}
foreach (var searchPath in pathVar.Split(":", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
string binaryPath = Path.Combine(searchPath, binary);
if (File.Exists(binaryPath))
{
return binaryPath;
}
}
return null;
}
public static int RunPkExec(string command)
{
if (PkExecPath == null)
{
return 1;
}
using Process process = new()
{
StartInfo =
{
FileName = PkExecPath,
ArgumentList = { "sh", "-c", command },
},
};
process.Start();
process.WaitForExit();
return process.ExitCode;
}
}
}

View file

@ -0,0 +1,160 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace Ryujinx.UI.Common.Helper
{
[SupportedOSPlatform("macos")]
public static partial class ObjectiveC
{
private const string ObjCRuntime = "/usr/lib/libobjc.A.dylib";
[LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)]
private static partial nint sel_getUid(string name);
[LibraryImport(ObjCRuntime, StringMarshalling = StringMarshalling.Utf8)]
private static partial nint objc_getClass(string name);
[LibraryImport(ObjCRuntime)]
private static partial void objc_msgSend(nint receiver, Selector selector);
[LibraryImport(ObjCRuntime)]
private static partial void objc_msgSend(nint receiver, Selector selector, byte value);
[LibraryImport(ObjCRuntime)]
private static partial void objc_msgSend(nint receiver, Selector selector, nint value);
[LibraryImport(ObjCRuntime)]
private static partial void objc_msgSend(nint receiver, Selector selector, NSRect point);
[LibraryImport(ObjCRuntime)]
private static partial void objc_msgSend(nint receiver, Selector selector, double value);
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
private static partial nint nint_objc_msgSend(nint receiver, Selector selector);
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
private static partial nint nint_objc_msgSend(nint receiver, Selector selector, nint param);
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend", StringMarshalling = StringMarshalling.Utf8)]
private static partial nint nint_objc_msgSend(nint receiver, Selector selector, string param);
[LibraryImport(ObjCRuntime, EntryPoint = "objc_msgSend")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool bool_objc_msgSend(nint receiver, Selector selector, nint param);
public readonly struct Object
{
public readonly nint ObjPtr;
private Object(nint pointer)
{
ObjPtr = pointer;
}
public Object(string name)
{
ObjPtr = objc_getClass(name);
}
public void SendMessage(Selector selector)
{
objc_msgSend(ObjPtr, selector);
}
public void SendMessage(Selector selector, byte value)
{
objc_msgSend(ObjPtr, selector, value);
}
public void SendMessage(Selector selector, Object obj)
{
objc_msgSend(ObjPtr, selector, obj.ObjPtr);
}
public void SendMessage(Selector selector, NSRect point)
{
objc_msgSend(ObjPtr, selector, point);
}
public void SendMessage(Selector selector, double value)
{
objc_msgSend(ObjPtr, selector, value);
}
public Object GetFromMessage(Selector selector)
{
return new Object(nint_objc_msgSend(ObjPtr, selector));
}
public Object GetFromMessage(Selector selector, Object obj)
{
return new Object(nint_objc_msgSend(ObjPtr, selector, obj.ObjPtr));
}
public Object GetFromMessage(Selector selector, NSString nsString)
{
return new Object(nint_objc_msgSend(ObjPtr, selector, nsString.StrPtr));
}
public Object GetFromMessage(Selector selector, string param)
{
return new Object(nint_objc_msgSend(ObjPtr, selector, param));
}
public bool GetBoolFromMessage(Selector selector, Object obj)
{
return bool_objc_msgSend(ObjPtr, selector, obj.ObjPtr);
}
}
public readonly struct Selector
{
public readonly nint SelPtr;
private Selector(string name)
{
SelPtr = sel_getUid(name);
}
public static implicit operator Selector(string value) => new(value);
}
public readonly struct NSString
{
public readonly nint StrPtr;
public NSString(string aString)
{
nint nsString = objc_getClass("NSString");
StrPtr = nint_objc_msgSend(nsString, "stringWithUTF8String:", aString);
}
public static implicit operator nint(NSString nsString) => nsString.StrPtr;
}
public readonly struct NSPoint
{
public readonly double X;
public readonly double Y;
public NSPoint(double x, double y)
{
X = x;
Y = y;
}
}
public readonly struct NSRect
{
public readonly NSPoint Pos;
public readonly NSPoint Size;
public NSRect(double x, double y, double width, double height)
{
Pos = new NSPoint(x, y);
Size = new NSPoint(width, height);
}
}
}
}

View file

@ -0,0 +1,115 @@
using Gommon;
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace Ryujinx.UI.Common.Helper
{
public static partial class OpenHelper
{
[LibraryImport("shell32.dll", SetLastError = true)]
private static partial int SHOpenFolderAndSelectItems(nint pidlFolder, uint cidl, nint apidl, uint dwFlags);
[LibraryImport("shell32.dll", SetLastError = true)]
private static partial void ILFree(nint pidlList);
[LibraryImport("shell32.dll", SetLastError = true)]
private static partial nint ILCreateFromPathW([MarshalAs(UnmanagedType.LPWStr)] string pszPath);
public static void OpenFolder(string path)
{
if (Directory.Exists(path))
{
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
Verb = "open",
});
}
else
{
Logger.Notice.Print(LogClass.Application, $"Directory \"{path}\" doesn't exist!");
}
}
public static void OpenFolder(FilePath path) => OpenFolder(path.Path);
public static void LocateFile(string path)
{
if (File.Exists(path))
{
if (OperatingSystem.IsWindows())
{
nint pidlList = ILCreateFromPathW(path);
if (pidlList != nint.Zero)
{
try
{
Marshal.ThrowExceptionForHR(SHOpenFolderAndSelectItems(pidlList, 0, nint.Zero, 0));
}
finally
{
ILFree(pidlList);
}
}
}
else if (OperatingSystem.IsMacOS())
{
ObjectiveC.NSString nsStringPath = new(path);
ObjectiveC.Object nsUrl = new("NSURL");
var urlPtr = nsUrl.GetFromMessage("fileURLWithPath:", nsStringPath);
ObjectiveC.Object nsArray = new("NSArray");
ObjectiveC.Object urlArray = nsArray.GetFromMessage("arrayWithObject:", urlPtr);
ObjectiveC.Object nsWorkspace = new("NSWorkspace");
ObjectiveC.Object sharedWorkspace = nsWorkspace.GetFromMessage("sharedWorkspace");
sharedWorkspace.SendMessage("activateFileViewerSelectingURLs:", urlArray);
}
else if (OperatingSystem.IsLinux())
{
Process.Start("dbus-send", $"--session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:\"file://{path}\" string:\"\"");
}
else
{
OpenFolder(Path.GetDirectoryName(path));
}
}
else
{
Logger.Notice.Print(LogClass.Application, $"File \"{path}\" doesn't exist!");
}
}
public static void OpenUrl(string url)
{
if (OperatingSystem.IsWindows())
{
Process.Start(new ProcessStartInfo("cmd", $"/c start {url.Replace("&", "^&")}"));
}
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", url);
}
else if (OperatingSystem.IsMacOS())
{
ObjectiveC.NSString nsStringPath = new(url);
ObjectiveC.Object nsUrl = new("NSURL");
var urlPtr = nsUrl.GetFromMessage("URLWithString:", nsStringPath);
ObjectiveC.Object nsWorkspace = new("NSWorkspace");
ObjectiveC.Object sharedWorkspace = nsWorkspace.GetFromMessage("sharedWorkspace");
sharedWorkspace.GetBoolFromMessage("openURL:", urlPtr);
}
else
{
Logger.Notice.Print(LogClass.Application, $"Cannot open url \"{url}\" on this platform!");
}
}
}
}

View file

@ -0,0 +1,213 @@
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
};
/// <summary>
/// Used by <see cref="FormatFileSize"/>.
/// </summary>
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
/// <summary>
/// Creates a human-readable string from a <see cref="TimeSpan"/>.
/// </summary>
/// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
/// <returns>A formatted string that can be displayed in the UI.</returns>
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}";
}
/// <summary>
/// Creates a human-readable string from a <see cref="DateTime"/>.
/// </summary>
/// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
/// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
/// <returns>A formatted string that can be displayed in the UI.</returns>
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
{
culture ??= CultureInfo.CurrentCulture;
return utcDateTime?.ToLocalTime().ToString(culture);
}
/// <summary>
/// Creates a human-readable file size string.
/// </summary>
/// <param name="size">The file size in bytes.</param>
/// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
/// <returns>A human-readable file size string.</returns>
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
/// <summary>
/// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
/// </summary>
/// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
/// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
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;
}
/// <summary>
/// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
/// </summary>
/// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
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;
}
/// <summary>
/// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
/// </summary>
/// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
/// <returns>A <see cref="long"/> representing a number of bytes.</returns>
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
}
}