Add a dark theme option to the GUI

This commit is contained in:
whimsical-c4lic0 2026-02-17 02:10:27 -06:00
parent da40f3d0c5
commit b6872a2b9e
12 changed files with 343 additions and 123 deletions

View File

@ -0,0 +1,7 @@
namespace OF_DL.Enumerations;
public enum Theme
{
light,
dark
}

View File

@ -89,6 +89,9 @@ public class Config : IFileNameFormatConfig
[JsonConverter(typeof(StringEnumConverter))]
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error;
[JsonConverter(typeof(StringEnumConverter))]
public Theme Theme { get; set; } = Theme.light;
[ToggleableConfig] public bool IgnoreOwnMessages { get; set; }
[ToggleableConfig] public bool DisableBrowserAuth { get; set; }

View File

@ -236,6 +236,9 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"),
DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"),
// Appearance Settings
Theme = ParseTheme(hoconConfig.GetString("Appearance.Theme", "light")),
// Logging/Debug Settings
LoggingLevel = Enum.Parse<LoggingLevel>(hoconConfig.GetString("Logging.LoggingLevel"), true)
};
@ -407,6 +410,11 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
hocon.AppendLine($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}");
hocon.AppendLine("}");
hocon.AppendLine("# Appearance Settings");
hocon.AppendLine("Appearance {");
hocon.AppendLine($" Theme = \"{config.Theme.ToString().ToLower()}\"");
hocon.AppendLine("}");
hocon.AppendLine("# Logging/Debug Settings");
hocon.AppendLine("Logging {");
hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\"");
@ -500,6 +508,16 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
return Enum.Parse<VideoResolution>("_" + value, true);
}
private static Theme ParseTheme(string value)
{
if (Enum.TryParse(value, true, out Theme theme))
{
return theme;
}
return Theme.light;
}
private static double ParseDrmVideoDurationMatchThreshold(string value) =>
!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed)
? 0.98

View File

@ -3,6 +3,32 @@
xmlns:fluent="clr-namespace:Avalonia.Themes.Fluent;assembly=Avalonia.Themes.Fluent"
RequestedThemeVariant="Light"
x:Class="OF_DL.Gui.App">
<Application.Resources>
<SolidColorBrush x:Key="WindowBackgroundBrush" Color="#EEF3FB" />
<SolidColorBrush x:Key="SurfaceBackgroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="SurfaceBorderBrush" Color="#DDE5F3" />
<SolidColorBrush x:Key="PrimaryButtonBackgroundBrush" Color="#2E6EEA" />
<SolidColorBrush x:Key="PrimaryButtonForegroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="SecondaryButtonBackgroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="SecondaryButtonForegroundBrush" Color="#1F2A44" />
<SolidColorBrush x:Key="SecondaryButtonBorderBrush" Color="#CFD9EB" />
<SolidColorBrush x:Key="TopBarBackgroundBrush" Color="#DDEAFF" />
<SolidColorBrush x:Key="TopBarBorderBrush" Color="#CFD9EB" />
<SolidColorBrush x:Key="TopBarTextBrush" Color="#304261" />
<SolidColorBrush x:Key="TextPrimaryBrush" Color="#1F2A44" />
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#4A5B78" />
<SolidColorBrush x:Key="HelpBadgeBackgroundBrush" Color="#EAF0FB" />
<SolidColorBrush x:Key="HelpBadgeBorderBrush" Color="#C5D4EC" />
<SolidColorBrush x:Key="ErrorTextBrush" Color="#FF5A5A" />
<SolidColorBrush x:Key="PreviewBackgroundBrush" Color="#F5F8FE" />
<SolidColorBrush x:Key="PreviewBorderBrush" Color="#D8E3F4" />
<SolidColorBrush x:Key="DangerSoftBackgroundBrush" Color="#FFE8E8" />
<SolidColorBrush x:Key="DangerSoftBorderBrush" Color="#E8C5C5" />
<SolidColorBrush x:Key="DangerButtonBackgroundBrush" Color="#D84E4E" />
<SolidColorBrush x:Key="OverlayBackgroundBrush" Color="#80000000" />
<SolidColorBrush x:Key="ModalBackgroundBrush" Color="#FFFFFF" />
<SolidColorBrush x:Key="ModalBorderBrush" Color="#DDE5F3" />
</Application.Resources>
<Application.Styles>
<fluent:FluentTheme />
</Application.Styles>

View File

@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
using System.Reflection;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Newtonsoft.Json;
@ -41,7 +43,9 @@ public partial class ConfigFieldViewModel : ViewModelBase
{
["_240"] = "240p",
["_720"] = "720p",
["source"] = "Source Resolution"
["source"] = "Source Resolution",
["light"] = "Light",
["dark"] = "Dark"
};
public ConfigFieldViewModel(
@ -470,6 +474,7 @@ public partial class ConfigFieldViewModel : ViewModelBase
HashSet<string> allowedVariables = new(GetAllowedFileNameVariables(), StringComparer.OrdinalIgnoreCase);
HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase);
(string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors();
MatchCollection matches = s_fileNameVariableRegex.Matches(TextValue);
int currentIndex = 0;
@ -478,13 +483,13 @@ public partial class ConfigFieldViewModel : ViewModelBase
if (match.Index > currentIndex)
{
string plainText = TextValue[currentIndex..match.Index];
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(plainText, "#1F2A44"));
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor));
}
string variableName = match.Groups[1].Value;
bool isAllowedVariable = allowedVariables.Contains(variableName);
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(match.Value,
isAllowedVariable ? "#2E6EEA" : "#D84E4E"));
isAllowedVariable ? AllowedVariableColor : InvalidVariableColor));
if (!isAllowedVariable)
{
@ -497,7 +502,7 @@ public partial class ConfigFieldViewModel : ViewModelBase
if (currentIndex < TextValue.Length)
{
string trailingText = TextValue[currentIndex..];
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(trailingText, "#1F2A44"));
FileNameFormatSegments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor));
}
if (unknownVariables.Count > 0)
@ -507,6 +512,15 @@ public partial class ConfigFieldViewModel : ViewModelBase
}
}
private static (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor)
GetFileNamePreviewColors()
{
bool isDarkTheme = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark;
return isDarkTheme
? ("#DCE6F7", "#66A6FF", "#FF8C8C")
: ("#1F2A44", "#2E6EEA", "#D84E4E");
}
private static string ToDisplayName(string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))

View File

@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OF_DL.Models.Config;
@ -254,6 +256,7 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
HashSet<string> allowedSet = new(allowedVariables, StringComparer.OrdinalIgnoreCase);
HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase);
(string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors();
MatchCollection matches = s_fileNameVariableRegex.Matches(format);
int currentIndex = 0;
@ -262,12 +265,13 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
if (match.Index > currentIndex)
{
string plainText = format[currentIndex..match.Index];
segments.Add(new FileNameFormatSegmentViewModel(plainText, "#1F2A44"));
segments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor));
}
string variableName = match.Groups[1].Value;
bool isAllowed = allowedSet.Contains(variableName);
segments.Add(new FileNameFormatSegmentViewModel(match.Value, isAllowed ? "#2E6EEA" : "#D84E4E"));
segments.Add(new FileNameFormatSegmentViewModel(match.Value,
isAllowed ? AllowedVariableColor : InvalidVariableColor));
if (!isAllowed)
{
@ -280,7 +284,7 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
if (currentIndex < format.Length)
{
string trailingText = format[currentIndex..];
segments.Add(new FileNameFormatSegmentViewModel(trailingText, "#1F2A44"));
segments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor));
}
if (unknownVariables.Count > 0)
@ -289,4 +293,13 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
setUnknownMessage($"Unknown variable(s): {tokens}");
}
}
private static (string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor)
GetFileNamePreviewColors()
{
bool isDarkTheme = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark;
return isDarkTheme
? ("#DCE6F7", "#66A6FF", "#FF8C8C")
: ("#1F2A44", "#2E6EEA", "#D84E4E");
}
}

View File

@ -1,11 +1,15 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Avalonia.Threading;
using Newtonsoft.Json;
using OF_DL.Enumerations;
using OF_DL.Gui.Services;
using OF_DL.Models;
using OF_DL.Models.Config;
@ -124,10 +128,68 @@ public partial class MainWindowViewModel(
"Enable download speed limiting.",
[nameof(Config.DownloadLimitInMbPerSec)] =
"Download rate limit in MB/s when rate limiting is enabled.",
[nameof(Config.Theme)] =
"GUI theme for the configuration and download screens.",
[nameof(Config.LoggingLevel)] =
"Log verbosity written to logs/OFDL.txt."
};
private static readonly Dictionary<string, string> s_lightThemeBrushes = new(StringComparer.Ordinal)
{
["WindowBackgroundBrush"] = "#EEF3FB",
["SurfaceBackgroundBrush"] = "#FFFFFF",
["SurfaceBorderBrush"] = "#DDE5F3",
["PrimaryButtonBackgroundBrush"] = "#2E6EEA",
["PrimaryButtonForegroundBrush"] = "#FFFFFF",
["SecondaryButtonBackgroundBrush"] = "#FFFFFF",
["SecondaryButtonForegroundBrush"] = "#1F2A44",
["SecondaryButtonBorderBrush"] = "#CFD9EB",
["TopBarBackgroundBrush"] = "#DDEAFF",
["TopBarBorderBrush"] = "#CFD9EB",
["TopBarTextBrush"] = "#304261",
["TextPrimaryBrush"] = "#1F2A44",
["TextSecondaryBrush"] = "#4A5B78",
["HelpBadgeBackgroundBrush"] = "#EAF0FB",
["HelpBadgeBorderBrush"] = "#C5D4EC",
["ErrorTextBrush"] = "#FF5A5A",
["PreviewBackgroundBrush"] = "#F5F8FE",
["PreviewBorderBrush"] = "#D8E3F4",
["DangerSoftBackgroundBrush"] = "#FFE8E8",
["DangerSoftBorderBrush"] = "#E8C5C5",
["DangerButtonBackgroundBrush"] = "#D84E4E",
["OverlayBackgroundBrush"] = "#80000000",
["ModalBackgroundBrush"] = "#FFFFFF",
["ModalBorderBrush"] = "#DDE5F3"
};
private static readonly Dictionary<string, string> s_darkThemeBrushes = new(StringComparer.Ordinal)
{
["WindowBackgroundBrush"] = "#0F141D",
["SurfaceBackgroundBrush"] = "#151C28",
["SurfaceBorderBrush"] = "#2A3445",
["PrimaryButtonBackgroundBrush"] = "#4C8DFF",
["PrimaryButtonForegroundBrush"] = "#FFFFFF",
["SecondaryButtonBackgroundBrush"] = "#1C2533",
["SecondaryButtonForegroundBrush"] = "#DCE6F7",
["SecondaryButtonBorderBrush"] = "#33425A",
["TopBarBackgroundBrush"] = "#1A2433",
["TopBarBorderBrush"] = "#33425A",
["TopBarTextBrush"] = "#C7D6EE",
["TextPrimaryBrush"] = "#DCE6F7",
["TextSecondaryBrush"] = "#A8B8D2",
["HelpBadgeBackgroundBrush"] = "#233145",
["HelpBadgeBorderBrush"] = "#3A4E6A",
["ErrorTextBrush"] = "#FF8C8C",
["PreviewBackgroundBrush"] = "#1B2636",
["PreviewBorderBrush"] = "#314359",
["DangerSoftBackgroundBrush"] = "#3A2024",
["DangerSoftBorderBrush"] = "#6A3A40",
["DangerButtonBackgroundBrush"] = "#CC4A4A",
["OverlayBackgroundBrush"] = "#99000000",
["ModalBackgroundBrush"] = "#151C28",
["ModalBorderBrush"] = "#2A3445"
};
private Dictionary<string, long> _allUsers = [];
private Dictionary<string, long> _allLists = [];
private StartupResult _startupResult = new();
@ -816,6 +878,7 @@ public partial class MainWindowViewModel(
private async Task BeginStartupAsync()
{
ApplyThemeFromConfigFileIfAvailable();
_configReturnScreen = CurrentScreen;
SetLoading("Loading configuration...");
BuildConfigFields(configService.CurrentConfig);
@ -841,6 +904,34 @@ public partial class MainWindowViewModel(
await EnsureAuthenticationAndLoadUsersAsync();
}
private static void ApplyThemeFromConfigFileIfAvailable()
{
const string configPath = "config.conf";
if (!File.Exists(configPath))
{
return;
}
try
{
string configText = File.ReadAllText(configPath);
Match match = Regex.Match(configText, @"(?im)^\s*Theme\s*=\s*""(light|dark)""");
if (!match.Success)
{
return;
}
if (Enum.TryParse(match.Groups[1].Value, true, out Theme parsedTheme))
{
ApplyConfiguredTheme(parsedTheme);
}
}
catch
{
// Ignore theme parsing errors here; full config validation happens in ConfigService.
}
}
private async Task EnsureAuthenticationAndLoadUsersAsync()
{
bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync();
@ -1070,6 +1161,8 @@ public partial class MainWindowViewModel(
private void BuildConfigFields(Config config)
{
ApplyConfiguredTheme(config.Theme);
ConfigFields.Clear();
ConfigCategories.Clear();
ConfigCategoriesLeft.Clear();
@ -1311,6 +1404,23 @@ public partial class MainWindowViewModel(
private static string EscapePathForConfig(string path) =>
path.Replace(@"\", @"\\");
private static void ApplyConfiguredTheme(Theme theme)
{
if (Application.Current == null)
{
return;
}
bool useDarkTheme = theme == Theme.dark;
Application.Current.RequestedThemeVariant = useDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
Dictionary<string, string> palette = useDarkTheme ? s_darkThemeBrushes : s_lightThemeBrushes;
foreach (KeyValuePair<string, string> brush in palette)
{
Application.Current.Resources[brush.Key] = new SolidColorBrush(Color.Parse(brush.Value));
}
}
private static string GetConfigHelpText(string propertyName) =>
s_configHelpTextByProperty.TryGetValue(propertyName, out string? helpText)
? helpText
@ -1537,6 +1647,8 @@ public partial class MainWindowViewModel(
nameof(Config.LimitDownloadRate) => "Performance",
nameof(Config.DownloadLimitInMbPerSec) => "Performance",
nameof(Config.Theme) => "Appearance",
nameof(Config.LoggingLevel) => "Logging",
_ => "Other"
@ -1553,7 +1665,8 @@ public partial class MainWindowViewModel(
"Folder Structure" => 5,
"Subscriptions" => 6,
"Performance" => 7,
"Logging" => 8,
"Appearance" => 8,
"Logging" => 9,
_ => 100
};
}

View File

@ -11,30 +11,30 @@
MinWidth="1150"
MinHeight="700"
Title="OF DL"
Background="#EEF3FB"
Background="{DynamicResource WindowBackgroundBrush}"
mc:Ignorable="d">
<Window.Styles>
<Style Selector="Border.surface">
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#DDE5F3" />
<Setter Property="Background" Value="{DynamicResource SurfaceBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="#2E6EEA" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="Background" Value="{DynamicResource PrimaryButtonBackgroundBrush}" />
<Setter Property="Foreground" Value="{DynamicResource PrimaryButtonForegroundBrush}" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.secondary">
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="Foreground" Value="#1F2A44" />
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBackgroundBrush}" />
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonForegroundBrush}" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BorderBrush" Value="#CFD9EB" />
<Setter Property="BorderBrush" Value="{DynamicResource SecondaryButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
@ -57,11 +57,11 @@
<Border Grid.Row="1"
Padding="16"
BorderThickness="0,0,0,1"
BorderBrush="#CFD9EB"
Background="#DDEAFF">
BorderBrush="{DynamicResource TopBarBorderBrush}"
Background="{DynamicResource TopBarBackgroundBrush}">
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<TextBlock Grid.Column="0"
Foreground="#304261"
Foreground="{DynamicResource TopBarTextBrush}"
FontWeight="SemiBold"
Text="{Binding AuthenticatedUserDisplay}"
TextWrapping="Wrap"
@ -106,10 +106,10 @@
<Grid ColumnDefinitions="3*,5*" Margin="0,0,0,2">
<Border Grid.Column="0" Classes="surface" Padding="12" Margin="0,0,10,0">
<StackPanel Spacing="8">
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#1F2A44" Text="External" />
<TextBlock FontSize="16" FontWeight="Bold" Foreground="{DynamicResource TextPrimaryBrush}" Text="External" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFmpeg Path" />
<Button Width="20"
Height="20"
@ -120,8 +120,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -139,13 +139,13 @@
Click="OnBrowseFfmpegPathClick" />
</Grid>
<TextBlock IsVisible="{Binding HasFfmpegPathError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding FfmpegPathError}"
TextWrapping="Wrap" />
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
<TextBlock FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFprobe Path" />
<Button Width="20"
Height="20"
@ -156,8 +156,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -175,7 +175,7 @@
Click="OnBrowseFfprobePathClick" />
</Grid>
<TextBlock IsVisible="{Binding HasFfprobePathError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding FfprobePathError}"
TextWrapping="Wrap" />
</StackPanel>
@ -185,11 +185,11 @@
<StackPanel Spacing="8">
<TextBlock FontSize="16"
FontWeight="Bold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Download Media Types" />
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Media Types" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Media Types" />
<ItemsControl ItemsSource="{Binding MediaTypeOptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -211,8 +211,8 @@
VerticalContentAlignment="Center"
FontSize="11"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="9"
Content="?"
@ -222,13 +222,13 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock IsVisible="{Binding HasMediaTypesError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding MediaTypesError}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Sources" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Sources" />
<ItemsControl ItemsSource="{Binding MediaSourceOptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -250,8 +250,8 @@
VerticalContentAlignment="Center"
FontSize="11"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="9"
Content="?"
@ -261,7 +261,7 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock IsVisible="{Binding HasMediaSourcesError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding MediaSourcesError}"
TextWrapping="Wrap" />
</StackPanel>
@ -285,14 +285,14 @@
<StackPanel Spacing="8">
<TextBlock FontSize="16"
FontWeight="Bold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding CategoryName}" />
<StackPanel IsVisible="{Binding IsDownloadBehavior}"
Spacing="4"
Margin="0,0,0,10">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Download Path" />
<Button Width="20"
Height="20"
@ -303,8 +303,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -324,7 +324,7 @@
</Grid>
<TextBlock
IsVisible="{Binding ViewModel.HasDownloadPathError, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=False}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding ViewModel.DownloadPathError, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}"
TextWrapping="Wrap" />
@ -335,7 +335,7 @@
ClipToBounds="True">
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="DRM Video Duration Match Threshold"
TextWrapping="Wrap"
VerticalAlignment="Top" />
@ -352,8 +352,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -368,7 +368,7 @@
Margin="10,0,0,0"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="#4A5B78"
Foreground="{DynamicResource TextSecondaryBrush}"
Text="{Binding ViewModel.DrmVideoDurationMatchThresholdPercentLabel, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue='98%'}" />
</Grid>
</Grid>
@ -382,7 +382,7 @@
Spacing="6"
VerticalAlignment="Center">
<TextBlock FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Download Posts from Specific Dates"
VerticalAlignment="Center"
TextWrapping="Wrap" />
@ -397,8 +397,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -435,7 +435,7 @@
ClipToBounds="True">
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Limit Download Rate"
TextWrapping="Wrap"
VerticalAlignment="Top" />
@ -453,8 +453,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -472,7 +472,7 @@
Increment="1"
FormatString="N0" />
<TextBlock Text="Mbps"
Foreground="#4A5B78"
Foreground="{DynamicResource TextSecondaryBrush}"
VerticalAlignment="Center" />
</StackPanel>
</Grid>
@ -486,7 +486,7 @@
ClipToBounds="True">
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Create Separate Folders For Each"
TextWrapping="Wrap"
VerticalAlignment="Top" />
@ -504,8 +504,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -537,7 +537,7 @@
ClipToBounds="True">
<TextBlock Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding DisplayName}"
TextWrapping="Wrap"
VerticalAlignment="Top" />
@ -555,8 +555,8 @@
VerticalContentAlignment="Center"
FontSize="12"
FontWeight="Bold"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="10"
Content="?"
@ -594,7 +594,7 @@
Increment="1"
FormatString="N0" />
<TextBlock Text="seconds"
Foreground="#4A5B78"
Foreground="{DynamicResource TextSecondaryBrush}"
VerticalAlignment="Center" />
</StackPanel>
@ -633,8 +633,8 @@
FontWeight="SemiBold"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Content="⟳"
@ -659,8 +659,8 @@
</Grid>
<Border Padding="8"
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl
@ -686,7 +686,7 @@
<TextBlock
IsVisible="{Binding HasUnknownFileNameVariables}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownFileNameVariablesMessage}"
TextWrapping="Wrap" />
@ -719,8 +719,8 @@
<DataTemplate
x:DataType="vm:CreatorConfigRowViewModel">
<Border
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="10">
@ -729,7 +729,7 @@
<TextBlock
Grid.Column="0"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding Username}"
VerticalAlignment="Center" />
<Button
@ -744,8 +744,8 @@
FontWeight="SemiBold"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="#EAF0FB"
BorderBrush="#C5D4EC"
Background="{DynamicResource HelpBadgeBackgroundBrush}"
BorderBrush="{DynamicResource HelpBadgeBorderBrush}"
BorderThickness="1"
CornerRadius="6"
Content="✎"
@ -762,8 +762,8 @@
FontWeight="SemiBold"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="#FFE8E8"
BorderBrush="#E8C5C5"
Background="{DynamicResource DangerSoftBackgroundBrush}"
BorderBrush="{DynamicResource DangerSoftBorderBrush}"
BorderThickness="1"
CornerRadius="6"
Content="×"
@ -778,7 +778,7 @@
</StackPanel>
<TextBlock IsVisible="{Binding HasError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding ErrorMessage}"
TextWrapping="Wrap" />
</StackPanel>
@ -852,7 +852,7 @@
<Button Grid.Column="1"
IsVisible="{Binding IsDownloading}"
Classes="primary"
Background="#D84E4E"
Background="{DynamicResource DangerButtonBackgroundBrush}"
Command="{Binding StopWorkCommand}"
Content="Stop" />
</Grid>
@ -866,7 +866,7 @@
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Users" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Users" />
<CheckBox IsThreeState="True"
IsChecked="{Binding AllUsersSelected}"
Content="{Binding SelectedUsersSummary}"
@ -881,6 +881,8 @@
IsEnabled="{Binding !IsDownloading}" />
</Grid>
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding AvailableUsers}"
Background="{DynamicResource SurfaceBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceBorderBrush}"
IsEnabled="{Binding !IsDownloading}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
@ -902,8 +904,12 @@
Padding="10"
Classes="surface">
<Grid RowDefinitions="Auto,*">
<TextBlock Grid.Row="0" FontWeight="SemiBold" Foreground="#1F2A44" Text="Activity Log" />
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding ActivityLog}" />
<TextBlock Grid.Row="0" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Activity Log" />
<ListBox Grid.Row="1"
Margin="0,8,0,0"
ItemsSource="{Binding ActivityLog}"
Background="{DynamicResource SurfaceBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceBorderBrush}" />
</Grid>
</Border>
@ -914,7 +920,7 @@
Classes="surface"
IsVisible="{Binding IsDownloadProgressVisible}">
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44"
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding DownloadProgressDescription}" />
<ProgressBar IsIndeterminate="{Binding IsDownloadProgressIndeterminate}"
Minimum="0"
@ -944,11 +950,11 @@
<!-- Modal Overlay -->
<Grid Grid.Row="0" Grid.RowSpan="3"
IsVisible="{Binding CreatorConfigEditor.ModalViewModel.IsOpen}"
Background="#80000000"
Background="{DynamicResource OverlayBackgroundBrush}"
ZIndex="1000"
PointerPressed="OnModalOverlayClicked">
<Border Background="#FFFFFF"
BorderBrush="#DDE5F3"
<Border Background="{DynamicResource ModalBackgroundBrush}"
BorderBrush="{DynamicResource ModalBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="24"
@ -962,11 +968,11 @@
<StackPanel Spacing="14" Margin="8,4,8,4" x:DataType="vm:CreatorConfigModalViewModel">
<TextBlock FontSize="18"
FontWeight="SemiBold"
Foreground="#1F2A44"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding DialogTitle}" />
<StackPanel Spacing="6">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Username" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Username" />
<Grid ColumnDefinitions="*,Auto">
<AutoCompleteBox Grid.Column="0"
Text="{Binding Username}"
@ -975,17 +981,17 @@
FilterMode="Contains" />
</Grid>
<TextBlock IsVisible="{Binding HasUsernameError}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UsernameError}"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock FontWeight="SemiBold"
Foreground="#4A5B78"
Foreground="{DynamicResource TextSecondaryBrush}"
Text="Filename Formats (leave blank to use global defaults)" />
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Paid Post Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Post Filename Format" />
<TextBox Text="{Binding PaidPostFileNameFormat}"
Watermark="Optional: override global paid post format" />
<Grid ColumnDefinitions="*,Auto">
@ -1000,8 +1006,8 @@
Command="{Binding InsertPaidPostVariableCommand}" />
</Grid>
<Border Padding="8"
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PaidPostSegments}">
@ -1021,13 +1027,13 @@
</ItemsControl>
</Border>
<TextBlock IsVisible="{Binding HasUnknownPaidPostVariables}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPaidPostVariablesMessage}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Post Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Post Filename Format" />
<TextBox Text="{Binding PostFileNameFormat}"
Watermark="Optional: override global post format" />
<Grid ColumnDefinitions="*,Auto">
@ -1042,8 +1048,8 @@
Command="{Binding InsertPostVariableCommand}" />
</Grid>
<Border Padding="8"
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PostSegments}">
@ -1063,13 +1069,13 @@
</ItemsControl>
</Border>
<TextBlock IsVisible="{Binding HasUnknownPostVariables}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPostVariablesMessage}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Paid Message Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Message Filename Format" />
<TextBox Text="{Binding PaidMessageFileNameFormat}"
Watermark="Optional: override global paid message format" />
<Grid ColumnDefinitions="*,Auto">
@ -1084,8 +1090,8 @@
Command="{Binding InsertPaidMessageVariableCommand}" />
</Grid>
<Border Padding="8"
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding PaidMessageSegments}">
@ -1105,13 +1111,13 @@
</ItemsControl>
</Border>
<TextBlock IsVisible="{Binding HasUnknownPaidMessageVariables}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownPaidMessageVariablesMessage}"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Message Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Message Filename Format" />
<TextBox Text="{Binding MessageFileNameFormat}"
Watermark="Optional: override global message format" />
<Grid ColumnDefinitions="*,Auto">
@ -1126,8 +1132,8 @@
Command="{Binding InsertMessageVariableCommand}" />
</Grid>
<Border Padding="8"
Background="#F5F8FE"
BorderBrush="#D8E3F4"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1"
CornerRadius="8">
<ItemsControl ItemsSource="{Binding MessageSegments}">
@ -1147,7 +1153,7 @@
</ItemsControl>
</Border>
<TextBlock IsVisible="{Binding HasUnknownMessageVariables}"
Foreground="#FF5A5A"
Foreground="{DynamicResource ErrorTextBrush}"
Text="{Binding UnknownMessageVariablesMessage}"
TextWrapping="Wrap" />
</StackPanel>
@ -1167,3 +1173,4 @@
</Grid>
</Window>

View File

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -41,18 +40,14 @@ public partial class MainWindow : Window
return;
}
TopLevel? topLevel = TopLevel.GetTopLevel(this);
TopLevel? topLevel = GetTopLevel(this);
if (topLevel?.StorageProvider == null)
{
return;
}
IReadOnlyList<IStorageFile> selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
Title = "Select FFmpeg executable",
AllowMultiple = false
});
new FilePickerOpenOptions { Title = "Select FFmpeg executable", AllowMultiple = false });
IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
if (selectedFile == null)
@ -77,18 +72,14 @@ public partial class MainWindow : Window
return;
}
TopLevel? topLevel = TopLevel.GetTopLevel(this);
TopLevel? topLevel = GetTopLevel(this);
if (topLevel?.StorageProvider == null)
{
return;
}
IReadOnlyList<IStorageFile> selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
Title = "Select FFprobe executable",
AllowMultiple = false
});
new FilePickerOpenOptions { Title = "Select FFprobe executable", AllowMultiple = false });
IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
if (selectedFile == null)
@ -113,18 +104,14 @@ public partial class MainWindow : Window
return;
}
TopLevel? topLevel = TopLevel.GetTopLevel(this);
TopLevel? topLevel = GetTopLevel(this);
if (topLevel?.StorageProvider == null)
{
return;
}
IReadOnlyList<IStorageFolder> selectedFolders = await topLevel.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = "Select download folder",
AllowMultiple = false
});
new FolderPickerOpenOptions { Title = "Select download folder", AllowMultiple = false });
IStorageFolder? selectedFolder = selectedFolders.FirstOrDefault();
if (selectedFolder == null)
@ -154,9 +141,7 @@ public partial class MainWindow : Window
vm.CreatorConfigEditor.ModalViewModel?.CancelCommand?.Execute(null);
}
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e)
{
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
// Stop the event from bubbling up to the overlay
e.Handled = true;
}
}

View File

@ -23,6 +23,7 @@ public class ConfigServiceTests
Assert.Equal(service.CurrentConfig.LoggingLevel, loggingService.LastLevel);
Assert.Equal("", service.CurrentConfig.FFprobePath);
Assert.Equal(0.98, service.CurrentConfig.DrmVideoDurationMatchThreshold, 3);
Assert.Equal(Theme.light, service.CurrentConfig.Theme);
}
[Fact]
@ -80,6 +81,25 @@ public class ConfigServiceTests
Assert.Equal(0.95, service.CurrentConfig.DrmVideoDurationMatchThreshold, 3);
}
[Fact]
public async Task LoadConfigurationAsync_ParsesAppearanceTheme()
{
using TempFolder temp = new();
using CurrentDirectoryScope _ = new(temp.Path);
FakeLoggingService loggingService = new();
ConfigService service = new(loggingService);
await service.SaveConfigurationAsync();
string hocon = await File.ReadAllTextAsync("config.conf");
hocon = hocon.Replace("Theme = \"light\"", "Theme = \"dark\"");
await File.WriteAllTextAsync("config.conf", hocon);
bool result = await service.LoadConfigurationAsync([]);
Assert.True(result);
Assert.Equal(Theme.dark, service.CurrentConfig.Theme);
}
[Fact]
public void ApplyToggleableSelections_UpdatesConfigAndReturnsChange()
{

View File

@ -581,6 +581,17 @@ Allowed values: `true`, `false`
Description: Posts and messages that contain #ad or free trial links will be ignored if set to `true`
## Theme
Type: `string`
Default: `"light"`
Allowed values: `"light"`, `"dark"`
Description: Controls the OF-DL GUI theme.
Set to `"light"` for light mode or `"dark"` for dark mode.
## Timeout
Type: `integer`

View File

@ -68,5 +68,8 @@ information about what it does, its default value, and the allowed values.
- [LimitDownloadRate](/config/all-configuration-options#limitdownloadrate)
- [DownloadLimitInMbPerSec](/config/all-configuration-options#downloadlimitinmbpersec)
- Appearance
- [Theme](/config/all-configuration-options#theme)
- Logging
- [LoggingLevel](/config/all-configuration-options#logginglevel)