first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
using System;
using System.Globalization;
using Avalonia.Media.Imaging;
using Nitrox.Launcher.Models.Utils;
namespace Nitrox.Launcher.Models.Converters;
public class BitmapAssetValueConverter : Converter<BitmapAssetValueConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
null => null,
Bitmap when targetType.IsAssignableFrom(typeof(Bitmap)) => value,
string s when targetType.IsAssignableFrom(typeof(Bitmap)) => AssetHelper.GetAssetFromStream(s, static stream => new Bitmap(stream)),
_ => throw new NotSupportedException()
};
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Nitrox.Launcher.Models.Utils;
namespace Nitrox.Launcher.Models.Converters;
public sealed class BoolToIconConverter : MarkupExtension, IValueConverter
{
/// <summary>
/// String that will be outputted if the input boolean value is <c>true</c>
/// </summary>
public string True { get; set; }
/// <summary>
/// String that will be outputted if the input boolean value is <c>false</c>
/// </summary>
public string False { get; set; }
/// <summary>
/// Decides if the converter will inverse the input boolean value before computing the output
/// </summary>
public bool Invert { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not bool @bool)
{
return null;
}
if (Invert)
{
@bool = !@bool;
}
return AssetHelper.GetAssetFromStream(@bool ? True : False, static stream => new Bitmap(stream));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// A converter base class that provides itself as value to the XAML compiler.
/// </summary>
public abstract class Converter<TSelf> : MarkupExtension, IValueConverter
where TSelf : Converter<TSelf>, new()
{
private static TSelf Instance { get; } = new();
public sealed override object ProvideValue(IServiceProvider serviceProvider) => Instance;
public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture);
public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException();
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Formats the bound value as a relative date string from a DateTime value.
/// </summary>
public class DateToRelativeDateConverter : Converter<DateToRelativeDateConverter>
{
private const float DAYS_IN_YEAR = 365.2425f;
private const float MEAN_DAYS_IN_MONTH = DAYS_IN_YEAR / 12f;
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
DateTimeOffset date = value switch
{
DateTime dateTime => dateTime,
DateTimeOffset dateTimeOffset => dateTimeOffset,
DateOnly dateOnly => dateOnly.ToDateTime(TimeOnly.MinValue),
string text when DateTimeOffset.TryParse(text, out DateTimeOffset offset) => offset,
_ => throw new ArgumentException($"Value must be a {nameof(DateTime)} or {nameof(DateTimeOffset)}", nameof(value))
};
TimeSpan delta = DateTimeOffset.UtcNow - date.UtcDateTime;
return delta switch
{
{ TotalSeconds: < 1 } => "just now",
{ TotalSeconds: < 2 } => "a second ago",
{ TotalMinutes: < 1 } => $"{(int)delta.TotalSeconds} seconds ago",
{ TotalMinutes: < 2 } => "a minute ago",
{ TotalMinutes: < 45 } => $"{(int)delta.TotalMinutes} minutes ago",
{ TotalHours: < 1.5 } => "an hour ago",
{ TotalDays: < 1 } => $"{(int)delta.TotalHours} hours ago",
{ TotalDays: < 2 } => "yesterday",
{ TotalDays: < MEAN_DAYS_IN_MONTH } => $"{(int)delta.TotalDays} days ago",
{ TotalDays: < MEAN_DAYS_IN_MONTH * 2 } => "a month ago",
{ TotalDays: < DAYS_IN_YEAR } => $"{(int)(delta.TotalDays / MEAN_DAYS_IN_MONTH)} months ago",
{ TotalDays: < DAYS_IN_YEAR * 2 } => "a year ago",
_ => $"{(int)(delta.TotalDays / DAYS_IN_YEAR)} years ago"
};
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Removes duplicates by non-unique ToString values of the given list.
/// </summary>
public class DeduplicateConverter : Converter<DeduplicateConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not IEnumerable<object> list)
{
return value;
}
return list.DistinctBy(i => i.ToString());
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Returns true if values are equal to each other.
/// Or if value is singular, if parameter is equal to the value.
/// </summary>
public class EqualityConverter : Converter<EqualityConverter>, IMultiValueConverter
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => Equals(value, parameter);
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
foreach (object val1 in values)
{
foreach (object val2 in values)
{
if (ReferenceEquals(val1, val2))
{
continue;
}
if (!Equals(val1, val2))
{
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Formats the bound value as a string from an integer.
/// </summary>
public partial class IntToStringConverter : Converter<IntToStringConverter>
{
[GeneratedRegex("[^0-9]")]
private static partial Regex DigitReplaceRegex { get; }
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.ToString() ?? "";
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
{
return 0;
}
if (value is not string str)
{
str = value.ToString();
if (str is null)
{
return 0;
}
}
str = DigitReplaceRegex.Replace(str, "");
if (int.TryParse(str, out int result))
{
return result;
}
return 0;
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Returns true if value is of the type as given by parameter (or any if parameter is a collection of types).
/// </summary>
public class IsTypeConverter : Converter<IsTypeConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch (parameter)
{
case Type typeParameter:
return typeParameter.IsInstanceOfType(value);
case IEnumerable<Type> typeParameters:
{
foreach (Type type in typeParameters)
{
if (type.IsInstanceOfType(value))
{
return true;
}
}
return false;
}
default:
return new BindingNotification(new ArgumentException($"Expected {nameof(parameter)} to be a {typeof(Type).FullName}"), BindingErrorType.Error);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Avalonia.Media.Imaging;
using Nitrox.Launcher.Models.Utils;
using NitroxModel.Discovery.Models;
namespace Nitrox.Launcher.Models.Converters;
public class PlatformToIconConverter : Converter<PlatformToIconConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return AssetHelper.GetAssetFromStream(GetIconPathForPlatform(value as Platform?), static stream => new Bitmap(stream));
}
private static string GetIconPathForPlatform(Platform? platform) => platform switch
{
Platform.EPIC => "/Assets/Images/store-icons/epic.png",
Platform.STEAM => "/Assets/Images/store-icons/steam.png",
Platform.MICROSOFT => "/Assets/Images/store-icons/xbox.png",
Platform.DISCORD => "/Assets/Images/store-icons/discord.png",
_ => "/Assets/Images/store-icons/missing.png",
};
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Formats the bound value as "0 BoundValue 0 BoundValue" Margin from a Padding, used for TextBox styling.
/// </summary>
/// <remarks>
/// This converter is used to solve a niche issue with the styling of TextBoxes.
/// </remarks>
public class TextBoxPaddingToMarginConverter : Converter<TextBoxPaddingToMarginConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Thickness padding)
{
return value;
}
bool isNegative = parameter != null && bool.TryParse(parameter.ToString(), out bool result) && result;
double top = isNegative ? -padding.Top : padding.Top;
double bottom = isNegative ? -padding.Bottom : padding.Bottom;
return new Thickness(0, top, 0, bottom);
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections;
using System.Globalization;
using System.Linq;
namespace Nitrox.Launcher.Models.Converters;
public class ToIntConverter : Converter<ToIntConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
return value switch
{
int i => i,
string valueStr when int.TryParse(valueStr, out int result) => result,
ICollection list => list.Count,
IEnumerable enumerable => enumerable.Cast<object>().Count(),
_ => System.Convert.ToInt32(value)
};
}
catch
{
return 0;
}
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value;
}

View File

@@ -0,0 +1,45 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Data;
using NitroxModel.Helper;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Formats the bound value as a string using a specific formatting style.
/// </summary>
public class ToStringConverter : Converter<ToStringConverter>
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
{
return null;
}
if (value.GetType().IsEnum)
{
value = (value as Enum)?.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
}
if (value is not string sourceText)
{
sourceText = value?.ToString();
}
if (!targetType.IsAssignableTo(typeof(string)) || sourceText == null)
{
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
}
return parameter switch
{
"upper" => sourceText.ToUpperInvariant(),
"lower" => sourceText.ToLowerInvariant(),
_ => CultureManager.CultureInfo.TextInfo.ToTitleCase(sourceText.ToLower().Replace("_", " ")),
};
}
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value;
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
namespace Nitrox.Launcher.Models.Converters;
/// <summary>
/// Trims the value when retrieved by code but keeps the spaces in the input field intact for improved UX.
/// </summary>
/// <remarks>
/// This converter is unconventional (inverted converter) in that the value is converted for the backend.
/// The user wants to be able to input spaces while they're typing, but we don't want to save those spaces.
/// </remarks>
public class TrimConverter : Converter<TrimConverter>
{
private readonly Lock inOutCacheLock = new();
/// <summary>
/// Cache to remember the last known untrimmed value (here, the value) for trimmed values (here, the key).
/// </summary>
private readonly Dictionary<string, string> inOutCache = new();
/// <summary>
/// Converts trimmed value back to last known untrimmed value.
/// </summary>
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not string strValue)
{
return value;
}
lock (inOutCacheLock)
{
if (inOutCache.TryGetValue(strValue.Trim(), out string untrimmedValue))
{
strValue = untrimmedValue;
}
}
return strValue;
}
/// <summary>
/// Converts untrimmed value back to trimmed value.
/// </summary>
public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not string strValue)
{
return value;
}
if (!strValue.StartsWith(' ') && !strValue.EndsWith(' '))
{
// It's safe to reset cache now.
lock (inOutCacheLock)
{
inOutCache.Clear();
}
return strValue;
}
string trim = strValue.Trim();
lock (inOutCacheLock)
{
inOutCache[trim] = strValue;
}
return trim;
}
}