SHA256
Refactor Every namespace Keyword Usage
This commit is contained in:
1 parent
4e67cb9b09
commit
27be2141f7
44 files changed
+4619
-4545
No files matched your search
+69
-68
@@ -1,78 +1,79 @@
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for double input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class DoubleLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for double input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// Validates the given double value against this limit.
|
||||
/// </summary>
|
||||
internal abstract class DoubleLimit
|
||||
/// <param name="value">The parsed double value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(double value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyDoubleLimit : DoubleLimit
|
||||
{
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given double value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed double value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(double value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyDoubleLimit : DoubleLimit
|
||||
{
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
if (value > 0)
|
||||
{
|
||||
if (value > 0)
|
||||
{
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeDoubleLimit : DoubleLimit
|
||||
{
|
||||
public double? Min { get; }
|
||||
public double? Max { get; }
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeDoubleLimit(double? min, double? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
if (Min.HasValue && value < Min.Value)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 숫자여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Max.HasValue && value > Max.Value)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 숫자여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the double value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeDoubleLimit : DoubleLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public double? Min { get; }
|
||||
public double? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeDoubleLimit(double? min, double? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(double value, out string errorMessage)
|
||||
{
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 숫자여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 숫자여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 숫자여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3,75 +3,74 @@ using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
internal class DoubleInputValidator
|
||||
{
|
||||
internal class DoubleInputValidator
|
||||
private struct FieldEntry
|
||||
{
|
||||
private struct FieldEntry
|
||||
{
|
||||
public List<DoubleLimit> Limits;
|
||||
}
|
||||
public List<DoubleLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
public DoubleInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits)
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="DoubleLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the DoubleField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public DoubleInputValidator AddLimits(string fieldName, params DoubleLimit[] limits)
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
entry = new FieldEntry
|
||||
{
|
||||
entry = new FieldEntry
|
||||
{
|
||||
Limits = new List<DoubleLimit>()
|
||||
};
|
||||
Limits = new List<DoubleLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<DoubleField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
var limits = entry.Limits;
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var field = _rootVisualElement.Q<DoubleField>(fieldName);
|
||||
if (field == null)
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"DoubleField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
var limits = entry.Limits;
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
||||
/// Populates choices from enum names and provides typed access to the selected value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
{
|
||||
private readonly DropdownField? _dropdown;
|
||||
private readonly List<T> _values;
|
||||
private Action<T>? _onValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Binds a UI Toolkit <see cref="DropdownField"/> to an enum type.
|
||||
/// Populates choices from enum names and provides typed access to the selected value.
|
||||
/// The currently selected enum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The enum type to bind.</typeparam>
|
||||
internal class EnumDropdownBinder<T> where T : struct, Enum
|
||||
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||
? _values[_dropdown.index]
|
||||
: default;
|
||||
|
||||
/// <summary>
|
||||
/// Queries a <see cref="DropdownField"/> by name from the root VisualElement,
|
||||
/// populates it with enum names, and selects the default value.
|
||||
/// </summary>
|
||||
/// <param name="rootVisualElement">The root VisualElement loaded from the UXML bundle.</param>
|
||||
/// <param name="fieldName">The name of the DropdownField in the UXML.</param>
|
||||
/// <param name="defaultValue">The initial selected value. Defaults to the first enum member.</param>
|
||||
public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null)
|
||||
{
|
||||
private readonly DropdownField? _dropdown;
|
||||
private readonly List<T> _values;
|
||||
private Action<T>? _onValueChanged;
|
||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected enum value.
|
||||
/// </summary>
|
||||
public T Value => _dropdown is { index: >= 0 } && _dropdown.index < _values.Count
|
||||
? _values[_dropdown.index]
|
||||
: default;
|
||||
|
||||
/// <summary>
|
||||
/// Queries a <see cref="DropdownField"/> by name from the root VisualElement,
|
||||
/// populates it with enum names, and selects the default value.
|
||||
/// </summary>
|
||||
/// <param name="rootVisualElement">The root VisualElement loaded from the UXML bundle.</param>
|
||||
/// <param name="fieldName">The name of the DropdownField in the UXML.</param>
|
||||
/// <param name="defaultValue">The initial selected value. Defaults to the first enum member.</param>
|
||||
public EnumDropdownBinder(VisualElement rootVisualElement, string fieldName, T? defaultValue = null)
|
||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||
if (_dropdown == null)
|
||||
{
|
||||
_values = Enum.GetValues(typeof(T)).Cast<T>().ToList();
|
||||
|
||||
_dropdown = rootVisualElement.Q<DropdownField>(fieldName);
|
||||
if (_dropdown == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Error(
|
||||
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_dropdown.choices = _values.Select(v => v.ToString()).ToList();
|
||||
|
||||
// Register the callback BEFORE setting index so the initial selection fires it too
|
||||
_dropdown.RegisterValueChangedCallback(_ =>
|
||||
{
|
||||
_onValueChanged?.Invoke(Value);
|
||||
});
|
||||
|
||||
var initial = defaultValue ?? _values[0];
|
||||
_dropdown.index = _values.IndexOf(initial);
|
||||
Melon<RenderMod>.Logger.Error(
|
||||
$"DropdownField '{fieldName}'을(를) 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback that fires when the selection changes, providing the new enum value.
|
||||
/// </summary>
|
||||
public void OnValueChanged(Action<T> callback)
|
||||
{
|
||||
_onValueChanged = callback;
|
||||
_dropdown.choices = _values.Select(v => v.ToString()).ToList();
|
||||
|
||||
// Fire immediately with current value so the caller gets the initial state
|
||||
if (_dropdown != null)
|
||||
{
|
||||
callback(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected enum value and updates the dropdown index.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value to set.</param>
|
||||
public void SetValue(T value)
|
||||
// Register the callback BEFORE setting index so the initial selection fires it too
|
||||
_dropdown.RegisterValueChangedCallback(_ =>
|
||||
{
|
||||
if (_dropdown == null) return;
|
||||
int idx = _values.IndexOf(value);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_dropdown.index = idx;
|
||||
}
|
||||
_onValueChanged?.Invoke(Value);
|
||||
});
|
||||
|
||||
var initial = defaultValue ?? _values[0];
|
||||
_dropdown.index = _values.IndexOf(initial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback that fires when the selection changes, providing the new enum value.
|
||||
/// </summary>
|
||||
public void OnValueChanged(Action<T> callback)
|
||||
{
|
||||
_onValueChanged = callback;
|
||||
|
||||
// Fire immediately with current value so the caller gets the initial state
|
||||
if (_dropdown != null)
|
||||
{
|
||||
callback(Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the selected enum value and updates the dropdown index.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value to set.</param>
|
||||
public void SetValue(T value)
|
||||
{
|
||||
if (_dropdown == null) return;
|
||||
int idx = _values.IndexOf(value);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_dropdown.index = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,75 +3,74 @@ using System.Linq;
|
||||
using MelonLoader;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace ADOFAIRenderer.Data.Input
|
||||
namespace ADOFAIRenderer.Data.Input;
|
||||
|
||||
internal class IntegerInputValidator
|
||||
{
|
||||
internal class IntegerInputValidator
|
||||
private struct FieldEntry
|
||||
{
|
||||
private struct FieldEntry
|
||||
{
|
||||
public List<IntegerLimit> Limits;
|
||||
}
|
||||
public List<IntegerLimit> Limits;
|
||||
}
|
||||
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
private readonly VisualElement _rootVisualElement;
|
||||
private readonly Dictionary<string, FieldEntry> _fields = new();
|
||||
|
||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
public IntegerInputValidator(VisualElement rootVisualElement)
|
||||
{
|
||||
_rootVisualElement = rootVisualElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits)
|
||||
/// <summary>
|
||||
/// Registers one or more <see cref="IntegerLimit"/>s for the field with the given name.
|
||||
/// Multiple limits can be added to the same field — they are all checked in order.
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The name of the IntegerField in the UXML.</param>
|
||||
/// <param name="limits">The limits to apply.</param>
|
||||
public IntegerInputValidator AddLimits(string fieldName, params IntegerLimit[] limits)
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
{
|
||||
if (!_fields.TryGetValue(fieldName, out var entry))
|
||||
entry = new FieldEntry
|
||||
{
|
||||
entry = new FieldEntry
|
||||
{
|
||||
Limits = new List<IntegerLimit>()
|
||||
};
|
||||
Limits = new List<IntegerLimit>()
|
||||
};
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
{
|
||||
var field = _rootVisualElement.Q<IntegerField>(fieldName);
|
||||
if (field == null)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.Limits.AddRange(limits);
|
||||
_fields[fieldName] = entry;
|
||||
return this;
|
||||
}
|
||||
var limits = entry.Limits;
|
||||
|
||||
/// <summary>
|
||||
/// Queries all registered fields from the root VisualElement and
|
||||
/// registers value-changed callbacks that rollback invalid values.
|
||||
/// </summary>
|
||||
public void SetupValidation()
|
||||
{
|
||||
foreach (var (fieldName, entry) in _fields)
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var field = _rootVisualElement.Q<IntegerField>(fieldName);
|
||||
if (field == null)
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning(
|
||||
$"IntegerField '{fieldName}'을(를) 찾을 수 없습니다. 유효성 검사를 건너뜁니다.");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
var limits = entry.Limits;
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (limits.Any(limit => !limit.Validate(evt.newValue, out _)))
|
||||
{
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
field.SetValueWithoutNotify(evt.previousValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-68
@@ -1,78 +1,79 @@
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for integer input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// </summary>
|
||||
internal abstract class IntegerLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for integer input validation limits.
|
||||
/// Multiple limits can be composed on a single input field — all are checked in order.
|
||||
/// Validates the given integer value against this limit.
|
||||
/// </summary>
|
||||
internal abstract class IntegerLimit
|
||||
/// <param name="value">The parsed integer value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(int value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyIntegerLimit : IntegerLimit
|
||||
{
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the given integer value against this limit.
|
||||
/// </summary>
|
||||
/// <param name="value">The parsed integer value to validate.</param>
|
||||
/// <param name="errorMessage">
|
||||
/// When validation fails, set to a human-readable error message.
|
||||
/// When validation succeeds, set to null.
|
||||
/// </param>
|
||||
/// <returns>true if the value satisfies this limit; false otherwise.</returns>
|
||||
public abstract bool Validate(int value, out string errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be strictly positive (> 0).
|
||||
/// </summary>
|
||||
internal class PositiveOnlyIntegerLimit : IntegerLimit
|
||||
{
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
if (value > 0)
|
||||
{
|
||||
if (value > 0)
|
||||
{
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeIntegerLimit : IntegerLimit
|
||||
{
|
||||
public int? Min { get; }
|
||||
public int? Max { get; }
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeIntegerLimit(int? min, int? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
if (Min.HasValue && value < Min.Value)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Max.HasValue && value > Max.Value)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
|
||||
errorMessage = "값은 0보다 큰 양의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requires the integer value to be within an inclusive [Min, Max] range.
|
||||
/// Either bound can be null to represent an unbounded side.
|
||||
/// </summary>
|
||||
internal class RangeIntegerLimit : IntegerLimit
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public int? Min { get; }
|
||||
public int? Max { get; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
|
||||
/// <param name="min">Inclusive lower bound, or null for no lower bound.</param>
|
||||
/// <param name="max">Inclusive upper bound, or null for no upper bound.</param>
|
||||
public RangeIntegerLimit(int? min, int? max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public override bool Validate(int value, out string errorMessage)
|
||||
{
|
||||
if (value < Min)
|
||||
{
|
||||
errorMessage = Max.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Min.Value} 이상의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value > Max)
|
||||
{
|
||||
errorMessage = Min.HasValue
|
||||
? $"값은 {Min.Value}에서 {Max.Value} 사이의 정수여야 합니다."
|
||||
: $"값은 {Max.Value} 이하의 정수여야 합니다.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+147
-148
@@ -7,184 +7,183 @@ using MelonLoader.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
internal class RenderPreset
|
||||
{
|
||||
internal class RenderPreset
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public RenderConfig Config { get; set; } = null!;
|
||||
}
|
||||
|
||||
internal static class PresetManager
|
||||
{
|
||||
private static string GetPresetsDirectory()
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public RenderConfig Config { get; set; } = null!;
|
||||
return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets");
|
||||
}
|
||||
|
||||
internal static class PresetManager
|
||||
private static string GetSafeFileName(string name)
|
||||
{
|
||||
private static string GetPresetsDirectory()
|
||||
return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
|
||||
}
|
||||
|
||||
public static void SavePreset(string name, RenderConfig config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return Path.Combine(MelonEnvironment.GameRootDirectory, "UserData", "ADOFAIRenderer", "Presets");
|
||||
throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다.");
|
||||
}
|
||||
|
||||
private static string GetSafeFileName(string name)
|
||||
string dir = GetPresetsDirectory();
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
public static void SavePreset(string name, RenderConfig config)
|
||||
var preset = new RenderPreset
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("프리셋 이름은 비어 있을 수 없습니다.");
|
||||
}
|
||||
Name = name,
|
||||
Config = config
|
||||
};
|
||||
|
||||
string dir = GetPresetsDirectory();
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
var preset = new RenderPreset
|
||||
{
|
||||
Name = name,
|
||||
Config = config
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
|
||||
public static List<RenderPreset> GetPresets()
|
||||
{
|
||||
var presets = new List<RenderPreset>();
|
||||
var dir = GetPresetsDirectory();
|
||||
public static List<RenderPreset> GetPresets()
|
||||
{
|
||||
var presets = new List<RenderPreset>();
|
||||
var dir = GetPresetsDirectory();
|
||||
|
||||
if (!Directory.Exists(dir)) return presets;
|
||||
if (!Directory.Exists(dir)) return presets;
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(dir, "*.json");
|
||||
foreach (var file in files)
|
||||
{
|
||||
var files = Directory.GetFiles(dir, "*.json");
|
||||
foreach (var file in files)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (jObject == null) continue;
|
||||
var json = File.ReadAllText(file);
|
||||
var jObject = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (jObject == null) continue;
|
||||
|
||||
var configToken = jObject["Config"];
|
||||
if (configToken is JObject configObj)
|
||||
{
|
||||
TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd");
|
||||
var configToken = jObject["Config"];
|
||||
if (configToken is JObject configObj)
|
||||
{
|
||||
TryChangeKey(configObj, "DelayAfterLevelEnd", "DelayAfterEnd");
|
||||
|
||||
// 1. legacy EncodingConfig -> VideoConfig
|
||||
if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj)
|
||||
{
|
||||
configObj["VideoConfig"] ??= oldEncodingConfigObj;
|
||||
configObj.Remove("EncodingConfig");
|
||||
}
|
||||
|
||||
// 2. legacy top-level Codec -> VideoConfig.Codec
|
||||
var legacyCodecToken = configObj["Codec"];
|
||||
if (legacyCodecToken != null && legacyCodecToken.Type != JTokenType.Null)
|
||||
{
|
||||
if (configObj["VideoConfig"] == null || configObj["VideoConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["VideoConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var videoConfigObj = (JObject)configObj["VideoConfig"]!;
|
||||
videoConfigObj["Codec"] ??= legacyCodecToken;
|
||||
|
||||
configObj.Remove("Codec");
|
||||
}
|
||||
|
||||
// 3. legacy top-level Audio properties -> AudioConfig
|
||||
string[] legacyAudioProps = { "RenderAudio", "AudioSampleRate", "AudioChannels", "AudioBitrate" };
|
||||
foreach (var prop in legacyAudioProps)
|
||||
{
|
||||
var legacyToken = configObj[prop];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
{
|
||||
if (configObj["AudioConfig"] == null || configObj["AudioConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["AudioConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var audioConfigObj = (JObject)configObj["AudioConfig"]!;
|
||||
audioConfigObj[prop] ??= legacyToken;
|
||||
}
|
||||
|
||||
configObj.Remove(prop);
|
||||
}
|
||||
|
||||
if (configObj["AudioConfig"] != null && configObj["AudioConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj.Remove("AudioConfig");
|
||||
}
|
||||
|
||||
// 4. legacy top-level Advanced properties -> AdvancedConfig
|
||||
string[] legacyAdvancedProps = { "RandomSeed", "ProcessAudio", "NonTile" };
|
||||
foreach (var prop in legacyAdvancedProps)
|
||||
{
|
||||
var legacyToken = configObj[prop];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
{
|
||||
if (configObj["AdvancedConfig"] == null || configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["AdvancedConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var advancedConfigObj = (JObject)configObj["AdvancedConfig"]!;
|
||||
advancedConfigObj[prop] ??= legacyToken;
|
||||
}
|
||||
|
||||
configObj.Remove(prop);
|
||||
}
|
||||
|
||||
if (configObj["AdvancedConfig"] != null && configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj.Remove("AdvancedConfig");
|
||||
}
|
||||
// 1. legacy EncodingConfig -> VideoConfig
|
||||
if (configObj["EncodingConfig"] is JObject oldEncodingConfigObj)
|
||||
{
|
||||
configObj["VideoConfig"] ??= oldEncodingConfigObj;
|
||||
configObj.Remove("EncodingConfig");
|
||||
}
|
||||
|
||||
var preset = jObject.ToObject<RenderPreset>();
|
||||
if (preset != null && !string.IsNullOrEmpty(preset.Name))
|
||||
|
||||
// 2. legacy top-level Codec -> VideoConfig.Codec
|
||||
var legacyCodecToken = configObj["Codec"];
|
||||
if (legacyCodecToken != null && legacyCodecToken.Type != JTokenType.Null)
|
||||
{
|
||||
presets.Add(preset);
|
||||
if (configObj["VideoConfig"] == null || configObj["VideoConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["VideoConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var videoConfigObj = (JObject)configObj["VideoConfig"]!;
|
||||
videoConfigObj["Codec"] ??= legacyCodecToken;
|
||||
|
||||
configObj.Remove("Codec");
|
||||
}
|
||||
|
||||
// 3. legacy top-level Audio properties -> AudioConfig
|
||||
string[] legacyAudioProps = { "RenderAudio", "AudioSampleRate", "AudioChannels", "AudioBitrate" };
|
||||
foreach (var prop in legacyAudioProps)
|
||||
{
|
||||
var legacyToken = configObj[prop];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
{
|
||||
if (configObj["AudioConfig"] == null || configObj["AudioConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["AudioConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var audioConfigObj = (JObject)configObj["AudioConfig"]!;
|
||||
audioConfigObj[prop] ??= legacyToken;
|
||||
}
|
||||
|
||||
configObj.Remove(prop);
|
||||
}
|
||||
|
||||
if (configObj["AudioConfig"] != null && configObj["AudioConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj.Remove("AudioConfig");
|
||||
}
|
||||
|
||||
// 4. legacy top-level Advanced properties -> AdvancedConfig
|
||||
string[] legacyAdvancedProps = { "RandomSeed", "ProcessAudio", "NonTile" };
|
||||
foreach (var prop in legacyAdvancedProps)
|
||||
{
|
||||
var legacyToken = configObj[prop];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
{
|
||||
if (configObj["AdvancedConfig"] == null || configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj["AdvancedConfig"] = new JObject();
|
||||
}
|
||||
|
||||
var advancedConfigObj = (JObject)configObj["AdvancedConfig"]!;
|
||||
advancedConfigObj[prop] ??= legacyToken;
|
||||
}
|
||||
|
||||
configObj.Remove(prop);
|
||||
}
|
||||
|
||||
if (configObj["AdvancedConfig"] != null && configObj["AdvancedConfig"]!.Type == JTokenType.Null)
|
||||
{
|
||||
configObj.Remove("AdvancedConfig");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
var preset = jObject.ToObject<RenderPreset>();
|
||||
if (preset != null && !string.IsNullOrEmpty(preset.Name))
|
||||
{
|
||||
MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}");
|
||||
presets.Add(preset);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Warning($"프리셋 파일 '{Path.GetFileName(file)}' 로드 중 오류 발생: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}");
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MelonLogger.Error($"프리셋 목록을 불러오는 중 오류 발생: {ex.Message}");
|
||||
}
|
||||
|
||||
private static void TryChangeKey(JObject configObj, string keyBefore, string targetKey)
|
||||
return presets;
|
||||
}
|
||||
|
||||
private static void TryChangeKey(JObject configObj, string keyBefore, string targetKey)
|
||||
{
|
||||
var legacyToken = configObj[keyBefore];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
configObj[targetKey] = legacyToken;
|
||||
|
||||
configObj.Remove(keyBefore);
|
||||
}
|
||||
|
||||
public static void DeletePreset(string name)
|
||||
{
|
||||
string dir = GetPresetsDirectory();
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var legacyToken = configObj[keyBefore];
|
||||
if (legacyToken != null && legacyToken.Type != JTokenType.Null)
|
||||
configObj[targetKey] = legacyToken;
|
||||
|
||||
configObj.Remove(keyBefore);
|
||||
}
|
||||
|
||||
public static void DeletePreset(string name)
|
||||
{
|
||||
string dir = GetPresetsDirectory();
|
||||
string safeName = GetSafeFileName(name);
|
||||
string filePath = Path.Combine(dir, safeName + ".json");
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
-55
@@ -1,71 +1,70 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ADOFAIRenderer.Util.FFmpeg;
|
||||
|
||||
namespace ADOFAIRenderer.Data
|
||||
namespace ADOFAIRenderer.Data;
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||
public class RenderConfig
|
||||
{
|
||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||
public class RenderConfig
|
||||
{
|
||||
public double RenderFPS { get; set; }
|
||||
public double VideoFPS { get; set; }
|
||||
public double RenderFPS { get; set; }
|
||||
public double VideoFPS { get; set; }
|
||||
|
||||
public int RenderWidth { get; set; }
|
||||
public int RenderHeight { get; set; }
|
||||
public int RenderWidth { get; set; }
|
||||
public int RenderHeight { get; set; }
|
||||
|
||||
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
||||
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
||||
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
||||
public VideoEncodingConfig VideoConfig { get; set; } = new();
|
||||
public AudioEncodingConfig AudioConfig { get; set; } = new();
|
||||
public AdvancedConfig AdvancedConfig { get; set; } = new();
|
||||
|
||||
public double DelayAfterEnd { get; set; }
|
||||
public bool ShowJudgementText { get; set; } = false;
|
||||
}
|
||||
public double DelayAfterEnd { get; set; }
|
||||
public bool ShowJudgementText { get; set; } = false;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||
public class AdvancedConfig
|
||||
{
|
||||
public int? RandomSeed { get; set; } = null;
|
||||
public bool ProcessAudio { get; set; } = true;
|
||||
public bool NonTile { get; set; } = false;
|
||||
public string FileExtension { get; set; } = "mkv";
|
||||
}
|
||||
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
|
||||
public class AdvancedConfig
|
||||
{
|
||||
public int? RandomSeed { get; set; } = null;
|
||||
public bool ProcessAudio { get; set; } = true;
|
||||
public bool NonTile { get; set; } = false;
|
||||
public string FileExtension { get; set; } = "mkv";
|
||||
}
|
||||
|
||||
public class VideoEncodingConfig
|
||||
{
|
||||
public Codec Codec { get; set; } = Codec.H264;
|
||||
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
||||
public class VideoEncodingConfig
|
||||
{
|
||||
public Codec Codec { get; set; } = Codec.H264;
|
||||
public RateControlMode RateControlMode { get; set; } = RateControlMode.CBR;
|
||||
|
||||
public int Bitrate { get; set; } = 30000;
|
||||
public int MaxBitrate { get; set; } = 50000;
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public int QP { get; set; } = 18;
|
||||
public double KeyframeInterval { get; set; } = 2.0;
|
||||
public bool ForceSoftwareEncoder { get; set; }
|
||||
public int Bitrate { get; set; } = 30000;
|
||||
public int MaxBitrate { get; set; } = 50000;
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public int QP { get; set; } = 18;
|
||||
public double KeyframeInterval { get; set; } = 2.0;
|
||||
public bool ForceSoftwareEncoder { get; set; }
|
||||
}
|
||||
|
||||
public class AudioEncodingConfig
|
||||
{
|
||||
private int _audioSampleRate = 44100;
|
||||
private int _audioChannels = 2;
|
||||
private int _audioBitrate = 256000;
|
||||
|
||||
public bool RenderAudio { get; set; } = true;
|
||||
|
||||
public int AudioSampleRate
|
||||
{
|
||||
get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate;
|
||||
set => _audioSampleRate = value <= 0 ? 44100 : value;
|
||||
}
|
||||
|
||||
public class AudioEncodingConfig
|
||||
public int AudioChannels
|
||||
{
|
||||
private int _audioSampleRate = 44100;
|
||||
private int _audioChannels = 2;
|
||||
private int _audioBitrate = 256000;
|
||||
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
||||
set => _audioChannels = value <= 0 ? 2 : value;
|
||||
}
|
||||
|
||||
public bool RenderAudio { get; set; } = true;
|
||||
|
||||
public int AudioSampleRate
|
||||
{
|
||||
get => _audioSampleRate <= 0 ? 44100 : _audioSampleRate;
|
||||
set => _audioSampleRate = value <= 0 ? 44100 : value;
|
||||
}
|
||||
|
||||
public int AudioChannels
|
||||
{
|
||||
get => _audioChannels <= 0 ? 2 : _audioChannels;
|
||||
set => _audioChannels = value <= 0 ? 2 : value;
|
||||
}
|
||||
|
||||
public int AudioBitrate
|
||||
{
|
||||
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
||||
set => _audioBitrate = value <= 0 ? 256000 : value;
|
||||
}
|
||||
public int AudioBitrate
|
||||
{
|
||||
get => _audioBitrate <= 0 ? 256000 : _audioBitrate;
|
||||
set => _audioBitrate = value <= 0 ? 256000 : value;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user