using AvParser.Core.Proxies;
using AvParser.Core.Settings;
using AvParser.Infrastructure.Logging;
using AvParser.Infrastructure.Storage;
using AvParser.UI.Localization;
using AvParser.UI.Responsive;
using AvParser.UI.Services;
using ReactiveUI;
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Concurrency;
using ReactiveUI.SourceGenerators;
using Serilog.Core;
namespace AvParser.UI.ViewModels;
/// Theme, logging level and where the app keeps its files.
public partial class SettingsViewModel : PageViewModel
{
private readonly ISettingsService _settings;
private readonly IThemeService _theme;
private readonly LoggingLevelSwitch _levelSwitch;
private readonly IProxyPool _proxyPool;
private readonly ILocalizationService _localization;
/// Selected theme. Applied immediately, not on an OK button.
[Reactive]
public partial LocalizedOption SelectedTheme { get; set; }
/// Selected UI language. Applied immediately.
[Reactive]
public partial LocalizedOption SelectedLanguage { get; set; }
/// Selected Serilog level name. Takes effect immediately.
[Reactive]
public partial string SelectedLogLevel { get; set; }
/// How the pool picks the next proxy.
[Reactive]
public partial LocalizedOption SelectedRotation { get; set; }
/// When proxy liveness is verified.
[Reactive]
public partial LocalizedOption SelectedHealthCheck { get; set; }
/// Whether the remote proxy feed is consulted.
[Reactive]
public partial bool UseProxyFeed { get; set; }
/// URL fetched to decide whether a proxy works.
[Reactive]
public partial string ProxyProbeUrl { get; set; }
/// Per-proxy probe timeout, in seconds.
[Reactive]
public partial int ProxyProbeTimeoutSeconds { get; set; }
/// How many probes run at once during a sweep.
[Reactive]
public partial int ProxyProbeConcurrency { get; set; }
/// How many working proxies the startup warm-up aims for.
[Reactive]
public partial int ProxyMinimumLive { get; set; }
/// Whether network parsers may run with no proxy available.
[Reactive]
public partial bool AllowDirectConnection { get; set; }
/// Creates the page.
public SettingsViewModel(
ISettingsService settings,
IThemeService theme,
IAppPaths paths,
LoggingLevelSwitch levelSwitch,
IProxyPool proxyPool,
ILocalizationService localization,
ISequencer? mainThread = null
)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_theme = theme ?? throw new ArgumentNullException(nameof(theme));
_levelSwitch = levelSwitch ?? throw new ArgumentNullException(nameof(levelSwitch));
_proxyPool = proxyPool ?? throw new ArgumentNullException(nameof(proxyPool));
_localization = localization ?? throw new ArgumentNullException(nameof(localization));
ArgumentNullException.ThrowIfNull(paths);
var scheduler = mainThread ?? RxSchedulers.MainThreadScheduler;
SettingsFile = paths.SettingsFile;
LogDirectory = paths.LogDirectory;
SelectedTheme = Option(Themes, theme.Current);
SelectedLanguage = Option(Languages, localization.Current);
SelectedLogLevel = settings.Current.MinimumLogLevel;
var current = settings.Current;
SelectedRotation = Option(Rotations, current.ProxyRotation);
SelectedHealthCheck = Option(HealthChecks, current.ProxyHealthCheck);
UseProxyFeed = current.ProxyUseFeed;
ProxyProbeUrl = current.ProxyProbeUrl;
ProxyProbeTimeoutSeconds = current.ProxyProbeTimeoutSeconds;
ProxyProbeConcurrency = current.ProxyProbeConcurrency;
ProxyMinimumLive = current.ProxyMinimumLive;
AllowDirectConnection = current.AllowDirectConnection;
this.WhenAnyValue(x => x.SelectedTheme).ObserveOn(scheduler).Subscribe(option => _theme.Apply(option.Value));
this.WhenAnyValue(x => x.SelectedLanguage)
.ObserveOn(scheduler)
.Subscribe(option => _localization.Apply(option.Value));
this.WhenAnyValue(x => x.SelectedLogLevel)
.Where(static level => !string.IsNullOrEmpty(level))
.DistinctUntilChanged()
.Subscribe(ApplyLogLevel);
// Keep the picker honest when the theme is flipped from the title-bar button.
theme.Changes.ObserveOn(scheduler).Subscribe(value => SelectedTheme = Option(Themes, value));
// Every proxy knob funnels through one handler: they all end up rebuilding the same
// ProxyOptions, and applying them one at a time would reconfigure the pool six times.
this.WhenAnyValue(
x => x.SelectedRotation,
x => x.SelectedHealthCheck,
x => x.UseProxyFeed,
x => x.ProxyProbeUrl,
x => x.ProxyProbeTimeoutSeconds,
x => x.ProxyProbeConcurrency,
x => x.ProxyMinimumLive,
x => x.AllowDirectConnection,
(_, _, _, _, _, _, _, _) => RxVoid.Default
)
.Throttle(TimeSpan.FromMilliseconds(200), scheduler)
.ObserveOn(scheduler)
.Subscribe(_ => ApplyProxySettings());
this.WhenAnyValue(x => x.SelectedRotation)
.Subscribe(_ => this.RaisePropertyChanged(nameof(RotationDescription)));
this.WhenAnyValue(x => x.SelectedHealthCheck)
.Subscribe(_ => this.RaisePropertyChanged(nameof(HealthCheckDescription)));
}
///
public override string TitleKey => "Page.Settings";
///
public override string IconKey => "IconSettings";
/// Theme options offered by the picker.
public IReadOnlyList> Themes { get; } = LocalizedOption.ForAll();
/// Language options offered by the picker.
public IReadOnlyList> Languages { get; } = LocalizedOption.ForAll();
/// Serilog level names, most to least verbose.
public IReadOnlyList LogLevels => AppLogging.AvailableLevels;
/// Rotation strategies offered by the picker.
public IReadOnlyList> Rotations { get; } = LocalizedOption.ForAll();
/// Liveness policies offered by the picker.
public IReadOnlyList> HealthChecks { get; } =
LocalizedOption.ForAll();
/// Explains the selected rotation in one line.
public string RotationDescription => Localizer.Instance[$"Settings.Rotation.{SelectedRotation.Value}"];
/// Explains the selected health-check policy in one line.
public string HealthCheckDescription => Localizer.Instance[$"Settings.HealthCheck.{SelectedHealthCheck.Value}"];
/// Describes the compact breakpoint band.
public string CompactHint => Localizer.Instance.Format("Settings.Breakpoint.CompactHint", MediumBreakpoint);
/// Describes the medium breakpoint band.
public string MediumHint =>
Localizer.Instance.Format("Settings.Breakpoint.MediumHint", MediumBreakpoint, ExpandedBreakpoint);
/// Describes the expanded breakpoint band.
public string ExpandedHint => Localizer.Instance.Format("Settings.Breakpoint.ExpandedHint", ExpandedBreakpoint);
/// Full path of the settings file.
public string SettingsFile { get; }
/// Directory holding rolling log files.
public string LogDirectory { get; }
/// Width in pixels at which the shell switches from compact to the icon rail.
public double MediumBreakpoint => ResponsiveLayout.MediumMinWidth;
/// Width in pixels at which the shell switches to the full sidebar.
public double ExpandedBreakpoint => ResponsiveLayout.ExpandedMinWidth;
///
protected override void OnLanguageChanged()
{
base.OnLanguageChanged();
foreach (
var name in new[]
{
nameof(RotationDescription),
nameof(HealthCheckDescription),
nameof(CompactHint),
nameof(MediumHint),
nameof(ExpandedHint),
}
)
{
this.RaisePropertyChanged(name);
}
}
private static LocalizedOption Option(IReadOnlyList> options, TValue value)
where TValue : struct, Enum => options.First(option => option.Value.Equals(value));
private void ApplyLogLevel(string level)
{
_levelSwitch.MinimumLevel = AppLogging.ParseLevel(level);
_settings.Update(current => current with { MinimumLogLevel = level });
}
private void ApplyProxySettings()
{
AppSettings? applied = null;
_settings.Update(current =>
{
applied = current with
{
ProxyRotation = SelectedRotation.Value,
ProxyHealthCheck = SelectedHealthCheck.Value,
ProxyUseFeed = UseProxyFeed,
ProxyProbeUrl = ProxyProbeUrl,
ProxyProbeTimeoutSeconds = ProxyProbeTimeoutSeconds,
ProxyProbeConcurrency = ProxyProbeConcurrency,
ProxyMinimumLive = ProxyMinimumLive,
AllowDirectConnection = AllowDirectConnection,
};
return applied;
});
// Update() short-circuits a no-op change, so fall back to the stored value: the pool must
// still be configured on the very first pass, when nothing has changed yet.
_proxyPool.Configure((applied ?? _settings.Current).ToProxyOptions());
}
}