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))] [JsonConverter(typeof(StringEnumConverter))]
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error; 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 IgnoreOwnMessages { get; set; }
[ToggleableConfig] public bool DisableBrowserAuth { 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"), LimitDownloadRate = hoconConfig.GetBoolean("Performance.LimitDownloadRate"),
DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"), DownloadLimitInMbPerSec = hoconConfig.GetInt("Performance.DownloadLimitInMbPerSec"),
// Appearance Settings
Theme = ParseTheme(hoconConfig.GetString("Appearance.Theme", "light")),
// Logging/Debug Settings // Logging/Debug Settings
LoggingLevel = Enum.Parse<LoggingLevel>(hoconConfig.GetString("Logging.LoggingLevel"), true) 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($" DownloadLimitInMbPerSec = {config.DownloadLimitInMbPerSec}");
hocon.AppendLine("}"); 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/Debug Settings");
hocon.AppendLine("Logging {"); hocon.AppendLine("Logging {");
hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\""); hocon.AppendLine($" LoggingLevel = \"{config.LoggingLevel.ToString().ToLower()}\"");
@ -500,6 +508,16 @@ public class ConfigService(ILoggingService loggingService) : IConfigService
return Enum.Parse<VideoResolution>("_" + value, true); 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) => private static double ParseDrmVideoDurationMatchThreshold(string value) =>
!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) !double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed)
? 0.98 ? 0.98

View File

@ -3,6 +3,32 @@
xmlns:fluent="clr-namespace:Avalonia.Themes.Fluent;assembly=Avalonia.Themes.Fluent" xmlns:fluent="clr-namespace:Avalonia.Themes.Fluent;assembly=Avalonia.Themes.Fluent"
RequestedThemeVariant="Light" RequestedThemeVariant="Light"
x:Class="OF_DL.Gui.App"> 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> <Application.Styles>
<fluent:FluentTheme /> <fluent:FluentTheme />
</Application.Styles> </Application.Styles>

View File

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

View File

@ -1,5 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using OF_DL.Models.Config; using OF_DL.Models.Config;
@ -254,6 +256,7 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
HashSet<string> allowedSet = new(allowedVariables, StringComparer.OrdinalIgnoreCase); HashSet<string> allowedSet = new(allowedVariables, StringComparer.OrdinalIgnoreCase);
HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase); HashSet<string> unknownVariables = new(StringComparer.OrdinalIgnoreCase);
(string PlainTextColor, string AllowedVariableColor, string InvalidVariableColor) = GetFileNamePreviewColors();
MatchCollection matches = s_fileNameVariableRegex.Matches(format); MatchCollection matches = s_fileNameVariableRegex.Matches(format);
int currentIndex = 0; int currentIndex = 0;
@ -262,12 +265,13 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
if (match.Index > currentIndex) if (match.Index > currentIndex)
{ {
string plainText = format[currentIndex..match.Index]; string plainText = format[currentIndex..match.Index];
segments.Add(new FileNameFormatSegmentViewModel(plainText, "#1F2A44")); segments.Add(new FileNameFormatSegmentViewModel(plainText, PlainTextColor));
} }
string variableName = match.Groups[1].Value; string variableName = match.Groups[1].Value;
bool isAllowed = allowedSet.Contains(variableName); 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) if (!isAllowed)
{ {
@ -280,7 +284,7 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
if (currentIndex < format.Length) if (currentIndex < format.Length)
{ {
string trailingText = format[currentIndex..]; string trailingText = format[currentIndex..];
segments.Add(new FileNameFormatSegmentViewModel(trailingText, "#1F2A44")); segments.Add(new FileNameFormatSegmentViewModel(trailingText, PlainTextColor));
} }
if (unknownVariables.Count > 0) if (unknownVariables.Count > 0)
@ -289,4 +293,13 @@ public partial class CreatorConfigModalViewModel : ViewModelBase
setUnknownMessage($"Unknown variable(s): {tokens}"); 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.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Avalonia.Threading; using Avalonia.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using OF_DL.Enumerations;
using OF_DL.Gui.Services; using OF_DL.Gui.Services;
using OF_DL.Models; using OF_DL.Models;
using OF_DL.Models.Config; using OF_DL.Models.Config;
@ -124,10 +128,68 @@ public partial class MainWindowViewModel(
"Enable download speed limiting.", "Enable download speed limiting.",
[nameof(Config.DownloadLimitInMbPerSec)] = [nameof(Config.DownloadLimitInMbPerSec)] =
"Download rate limit in MB/s when rate limiting is enabled.", "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)] = [nameof(Config.LoggingLevel)] =
"Log verbosity written to logs/OFDL.txt." "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> _allUsers = [];
private Dictionary<string, long> _allLists = []; private Dictionary<string, long> _allLists = [];
private StartupResult _startupResult = new(); private StartupResult _startupResult = new();
@ -816,6 +878,7 @@ public partial class MainWindowViewModel(
private async Task BeginStartupAsync() private async Task BeginStartupAsync()
{ {
ApplyThemeFromConfigFileIfAvailable();
_configReturnScreen = CurrentScreen; _configReturnScreen = CurrentScreen;
SetLoading("Loading configuration..."); SetLoading("Loading configuration...");
BuildConfigFields(configService.CurrentConfig); BuildConfigFields(configService.CurrentConfig);
@ -841,6 +904,34 @@ public partial class MainWindowViewModel(
await EnsureAuthenticationAndLoadUsersAsync(); 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() private async Task EnsureAuthenticationAndLoadUsersAsync()
{ {
bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync(); bool hasValidAuth = await TryLoadAndValidateExistingAuthAsync();
@ -1070,6 +1161,8 @@ public partial class MainWindowViewModel(
private void BuildConfigFields(Config config) private void BuildConfigFields(Config config)
{ {
ApplyConfiguredTheme(config.Theme);
ConfigFields.Clear(); ConfigFields.Clear();
ConfigCategories.Clear(); ConfigCategories.Clear();
ConfigCategoriesLeft.Clear(); ConfigCategoriesLeft.Clear();
@ -1311,6 +1404,23 @@ public partial class MainWindowViewModel(
private static string EscapePathForConfig(string path) => private static string EscapePathForConfig(string path) =>
path.Replace(@"\", @"\\"); 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) => private static string GetConfigHelpText(string propertyName) =>
s_configHelpTextByProperty.TryGetValue(propertyName, out string? helpText) s_configHelpTextByProperty.TryGetValue(propertyName, out string? helpText)
? helpText ? helpText
@ -1537,6 +1647,8 @@ public partial class MainWindowViewModel(
nameof(Config.LimitDownloadRate) => "Performance", nameof(Config.LimitDownloadRate) => "Performance",
nameof(Config.DownloadLimitInMbPerSec) => "Performance", nameof(Config.DownloadLimitInMbPerSec) => "Performance",
nameof(Config.Theme) => "Appearance",
nameof(Config.LoggingLevel) => "Logging", nameof(Config.LoggingLevel) => "Logging",
_ => "Other" _ => "Other"
@ -1553,7 +1665,8 @@ public partial class MainWindowViewModel(
"Folder Structure" => 5, "Folder Structure" => 5,
"Subscriptions" => 6, "Subscriptions" => 6,
"Performance" => 7, "Performance" => 7,
"Logging" => 8, "Appearance" => 8,
"Logging" => 9,
_ => 100 _ => 100
}; };
} }

View File

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

View File

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

View File

@ -23,6 +23,7 @@ public class ConfigServiceTests
Assert.Equal(service.CurrentConfig.LoggingLevel, loggingService.LastLevel); Assert.Equal(service.CurrentConfig.LoggingLevel, loggingService.LastLevel);
Assert.Equal("", service.CurrentConfig.FFprobePath); Assert.Equal("", service.CurrentConfig.FFprobePath);
Assert.Equal(0.98, service.CurrentConfig.DrmVideoDurationMatchThreshold, 3); Assert.Equal(0.98, service.CurrentConfig.DrmVideoDurationMatchThreshold, 3);
Assert.Equal(Theme.light, service.CurrentConfig.Theme);
} }
[Fact] [Fact]
@ -80,6 +81,25 @@ public class ConfigServiceTests
Assert.Equal(0.95, service.CurrentConfig.DrmVideoDurationMatchThreshold, 3); 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] [Fact]
public void ApplyToggleableSelections_UpdatesConfigAndReturnsChange() 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` 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 ## Timeout
Type: `integer` 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) - [LimitDownloadRate](/config/all-configuration-options#limitdownloadrate)
- [DownloadLimitInMbPerSec](/config/all-configuration-options#downloadlimitinmbpersec) - [DownloadLimitInMbPerSec](/config/all-configuration-options#downloadlimitinmbpersec)
- Appearance
- [Theme](/config/all-configuration-options#theme)
- Logging - Logging
- [LoggingLevel](/config/all-configuration-options#logginglevel) - [LoggingLevel](/config/all-configuration-options#logginglevel)