Add help links and pages

This commit is contained in:
whimsical-c4lic0 2026-02-17 17:14:45 -06:00
parent e58ac7d2a6
commit ac1c814633
9 changed files with 594 additions and 56 deletions

View File

@ -23,7 +23,7 @@ most important change points.
- `OF DL/Program.cs` is the single entrypoint and routes between GUI (default) and CLI (`--cli`).
- `OF DL/CLI/` contains Spectre.Console UI helpers and progress reporting (CLI-only).
- `OF DL.Gui/` contains the Avalonia desktop UI (`App`, `MainWindow`, MVVM view models, and GUI event handlers).
- `OF DL.Gui/` contains the Avalonia desktop UI (`App`, `MainWindow`, `AboutWindow`, `FaqWindow`, MVVM view models, and GUI event handlers).
- `OF DL.Core/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames).
- `OF DL.Core/Models/` holds configuration, auth, API request/response models, downloads/startup results, DTOs,
entities, and mapping helpers.
@ -232,6 +232,7 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and
- `OF DL/Program.cs` for the execution path and menu flow.
- `OF DL.Gui/ViewModels/MainWindowViewModel.cs` for GUI startup flow (config -> auth -> users/lists -> selection).
- `OF DL.Gui/Views/MainWindow.axaml` for GUI layout and interaction points.
- `OF DL.Gui/Views/AboutWindow.axaml` and `OF DL.Gui/Views/FaqWindow.axaml` for Help menu windows.
- `OF DL.Core/Services/ApiService.cs` for OF API calls and header signing.
- `OF DL.Core/Services/DownloadService.cs` for downloads and DRM handling.
- `OF DL.Core/Services/DownloadOrchestrationService.cs` for creator selection and flow control.

View File

@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
@ -26,9 +27,13 @@ public partial class MainWindowViewModel(
IStartupService startupService,
IDownloadOrchestrationService downloadOrchestrationService) : ViewModelBase
{
private const string UnknownToolVersion = "Not detected";
private static readonly string s_defaultDownloadPath = Path.GetFullPath(
Path.Combine(Directory.GetCurrentDirectory(), "__user_data__", "sites", "OnlyFans"));
private static readonly string s_programVersion = ResolveProgramVersion();
private static readonly (string DisplayName, string PropertyName)[] s_mediaTypeOptions =
[
("Videos", nameof(Config.DownloadVideos)),
@ -266,6 +271,16 @@ public partial class MainWindowViewModel(
public bool HidePrivateInfo { get; } = Program.HidePrivateInfo;
public string ProgramVersion => s_programVersion;
public string FfmpegVersion => string.IsNullOrWhiteSpace(_startupResult.FfmpegVersion)
? UnknownToolVersion
: _startupResult.FfmpegVersion;
public string FfprobeVersion => string.IsNullOrWhiteSpace(_startupResult.FfprobeVersion)
? UnknownToolVersion
: _startupResult.FfprobeVersion;
[ObservableProperty] private string? _selectedListName;
[ObservableProperty] private bool _hasInitialized;
@ -473,6 +488,11 @@ public partial class MainWindowViewModel(
[RelayCommand]
private void EditConfig()
{
if (CurrentScreen == AppScreen.Config)
{
return;
}
_configReturnScreen = CurrentScreen;
BuildConfigFields(configService.CurrentConfig);
ConfigScreenMessage = "Edit configuration values and save to apply changes.";
@ -958,6 +978,8 @@ public partial class MainWindowViewModel(
{
SetLoading("Validating environment...");
_startupResult = await startupService.ValidateEnvironmentAsync();
OnPropertyChanged(nameof(FfmpegVersion));
OnPropertyChanged(nameof(FfprobeVersion));
if (!_startupResult.IsWindowsVersionValid)
{
@ -1172,13 +1194,13 @@ public partial class MainWindowViewModel(
CreatorConfigEditor = new CreatorConfigEditorViewModel(_allUsers.Keys);
CreatorConfigEditor.LoadFromConfig(config.CreatorConfigs);
IEnumerable<System.Reflection.PropertyInfo> properties = typeof(Config)
IEnumerable<PropertyInfo> properties = typeof(Config)
.GetProperties()
.Where(property => property.CanRead && property.CanWrite)
.Where(property => !IsHiddenConfigField(property.Name))
.OrderBy(property => property.Name);
foreach (System.Reflection.PropertyInfo property in properties)
foreach (PropertyInfo property in properties)
{
object? value = property.GetValue(config);
IEnumerable<string> ignoredUsersListNames = property.Name == nameof(Config.IgnoredUsersListName)
@ -1300,7 +1322,7 @@ public partial class MainWindowViewModel(
private static bool GetBooleanConfigValue(Config config, string propertyName)
{
System.Reflection.PropertyInfo? property = typeof(Config).GetProperty(propertyName);
PropertyInfo? property = typeof(Config).GetProperty(propertyName);
if (property == null)
{
return false;
@ -1341,7 +1363,7 @@ public partial class MainWindowViewModel(
{
foreach (MultiSelectOptionViewModel option in options)
{
System.Reflection.PropertyInfo? property = typeof(Config).GetProperty(option.PropertyName);
PropertyInfo? property = typeof(Config).GetProperty(option.PropertyName);
if (property?.PropertyType == typeof(bool))
{
property.SetValue(config, option.IsSelected);
@ -1404,6 +1426,15 @@ public partial class MainWindowViewModel(
private static string EscapePathForConfig(string path) =>
path.Replace(@"\", @"\\");
private static string ResolveProgramVersion()
{
Version? version = Assembly.GetEntryAssembly()?.GetName().Version
?? typeof(MainWindowViewModel).Assembly.GetName().Version;
return version == null
? "Unknown"
: $"{version.Major}.{version.Minor}.{version.Build}";
}
private static void ApplyConfiguredTheme(Theme theme)
{
if (Application.Current == null)

View File

@ -0,0 +1,103 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:OF_DL.Gui.Views"
x:Class="OF_DL.Gui.Views.AboutWindow"
x:DataType="views:AboutWindow"
Width="760"
Height="520"
MinWidth="620"
MinHeight="440"
Title="About OF DL"
Background="{DynamicResource WindowBackgroundBrush}"
mc:Ignorable="d">
<Border Margin="14"
Padding="16"
Background="{DynamicResource SurfaceBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceBorderBrush}"
BorderThickness="1"
CornerRadius="12">
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="About OF DL" />
<Grid Grid.Row="1"
Margin="0,14,0,0"
ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="1"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Version:" />
<TextBlock Grid.Row="1"
Grid.Column="1"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding ProgramVersion}" />
<TextBlock Grid.Row="2"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Source Code:" />
<Button Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Left"
Content="{Binding SourceCodeUrl}"
Click="OnOpenSourceCodeClick" />
<TextBlock Grid.Row="3"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFmpeg Version:" />
<TextBlock Grid.Row="3"
Grid.Column="1"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding FfmpegVersion}" />
<TextBlock Grid.Row="4"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFprobe Version:" />
<TextBlock Grid.Row="4"
Grid.Column="1"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding FfprobeVersion}" />
<TextBlock Grid.Row="5"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFmpeg License:" />
<Button Grid.Row="5"
Grid.Column="1"
HorizontalAlignment="Left"
Content="{Binding FfmpegLicenseUrl}"
Click="OnOpenFfmpegLicenseClick" />
<TextBlock Grid.Row="6"
Grid.Column="0"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="FFprobe License:" />
<Button Grid.Row="6"
Grid.Column="1"
HorizontalAlignment="Left"
Content="{Binding FfprobeLicenseUrl}"
Click="OnOpenFfprobeLicenseClick" />
</Grid>
<Button Grid.Row="2"
Margin="0,18,0,0"
HorizontalAlignment="Right"
Content="Close"
Click="OnCloseClick" />
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,64 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform;
namespace OF_DL.Gui.Views;
public partial class AboutWindow : Window
{
private const string UnknownValue = "Unknown";
private const string UnknownToolVersion = "Not detected";
public string ProgramVersion { get; }
public string FfmpegVersion { get; }
public string FfprobeVersion { get; }
public string SourceCodeUrl { get; } = "https://git.ofdl.tools/sim0n00ps/OF-DL";
public string FfmpegLicenseUrl { get; } = "https://ffmpeg.org/legal.html";
public string FfprobeLicenseUrl { get; } = "https://ffmpeg.org/legal.html";
public AboutWindow()
: this(UnknownValue, UnknownToolVersion, UnknownToolVersion)
{
}
public AboutWindow(
string programVersion,
string ffmpegVersion,
string ffprobeVersion)
{
ProgramVersion = programVersion;
FfmpegVersion = ffmpegVersion;
FfprobeVersion = ffprobeVersion;
InitializeComponent();
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
DataContext = this;
}
private async void OnOpenSourceCodeClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(SourceCodeUrl);
private async void OnOpenFfmpegLicenseClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(FfmpegLicenseUrl);
private async void OnOpenFfprobeLicenseClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(FfprobeLicenseUrl);
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
private async Task OpenExternalUrlAsync(string url)
{
try
{
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
Process.Start(processStartInfo);
}
catch
{
await Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,107 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="using:OF_DL.Gui.Views"
x:Class="OF_DL.Gui.Views.FaqWindow"
x:DataType="views:FaqWindow"
Width="760"
Height="640"
MinWidth="600"
MinHeight="500"
Title="FAQ"
Background="{DynamicResource WindowBackgroundBrush}"
mc:Ignorable="d">
<Window.Styles>
<Style Selector="Border.faqCard">
<Setter Property="Background" Value="{DynamicResource SurfaceBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="14" />
<Setter Property="Margin" Value="0,0,0,10" />
</Style>
</Window.Styles>
<Border Margin="14"
Padding="16"
Background="{DynamicResource SurfaceBackgroundBrush}"
BorderBrush="{DynamicResource SurfaceBorderBrush}"
BorderThickness="1"
CornerRadius="12">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<TextBlock Grid.Row="0"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="Frequently Asked Questions" />
<ScrollViewer Grid.Row="2"
Margin="0,14,0,0">
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="views:FaqEntry">
<Border Classes="faqCard">
<StackPanel Spacing="8">
<TextBlock FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
Text="{Binding Question}"
TextWrapping="Wrap" />
<ItemsControl ItemsSource="{Binding Paragraphs}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
Text="{Binding .}"
TextWrapping="Wrap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl IsVisible="{Binding HasLinks}"
ItemsSource="{Binding Links}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="views:FaqLink">
<Button HorizontalAlignment="Left"
Background="Transparent"
BorderThickness="0"
Padding="0"
Foreground="{DynamicResource PrimaryButtonBackgroundBrush}"
FontWeight="SemiBold"
Content="{Binding Label}"
ToolTip.Tip="{Binding Url}"
CommandParameter="{Binding Url}"
Click="OnLinkClick" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border IsVisible="{Binding HasCodeSnippet}"
Margin="0,2,0,0"
Padding="10"
CornerRadius="8"
Background="{DynamicResource PreviewBackgroundBrush}"
BorderBrush="{DynamicResource PreviewBorderBrush}"
BorderThickness="1">
<TextBlock Text="{Binding CodeSnippet}"
FontFamily="Consolas"
FontSize="12"
Foreground="{DynamicResource TextPrimaryBrush}"
TextWrapping="Wrap" />
</Border>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="3"
Margin="0,18,0,0"
HorizontalAlignment="Right"
Content="Close"
Click="OnCloseClick" />
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,167 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform;
namespace OF_DL.Gui.Views;
public class FaqLink
{
public FaqLink(string label, string url)
{
Label = label;
Url = url;
}
public string Label { get; }
public string Url { get; }
}
public class FaqEntry
{
public FaqEntry(
string question,
IEnumerable<string> paragraphs,
IEnumerable<FaqLink>? links = null,
string? codeSnippet = null)
{
Question = question;
Paragraphs = paragraphs.ToList();
Links = links?.ToList() ?? [];
CodeSnippet = codeSnippet;
}
public string Question { get; }
public IReadOnlyList<string> Paragraphs { get; }
public IReadOnlyList<FaqLink> Links { get; }
public bool HasLinks => Links.Count > 0;
public string? CodeSnippet { get; }
public bool HasCodeSnippet => !string.IsNullOrWhiteSpace(CodeSnippet);
}
public partial class FaqWindow : Window
{
private const string DockerRunCommand =
"docker run --rm -it -v $HOME/ofdl/data/:/data -v $HOME/ofdl/config/:/config -p 8080:8080 git.ofdl.tools/sim0n00ps/of-dl:latest";
public FaqWindow()
{
InitializeComponent();
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://OF DL.Gui/Assets/icon.ico")));
BuildEntries();
DataContext = this;
}
public ObservableCollection<FaqEntry> Entries { get; } = [];
public string RuntimeSummary { get; private set; } = string.Empty;
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
private void BuildEntries()
{
Entries.Clear();
bool isDocker = string.Equals(
Environment.GetEnvironmentVariable("OFDL_DOCKER"),
"true",
StringComparison.OrdinalIgnoreCase);
bool isWindows = OperatingSystem.IsWindows();
Entries.Add(new FaqEntry(
"Why are some users missing from the users list?",
[
"Users may be missing from the users list if those subscriptions have expired or are restricted. You can enable expired subscriptions and restricted subscriptions from the \"Subscriptions\" section in \"Configuration\" (accessible from the Edit menu)."
]));
if (!isDocker)
{
Entries.Add(new FaqEntry(
"Where are the downloads saved?",
[
"Content is downloaded to the configured download directory. You can view or change the download directory from the \"Download Behavior\" section in \"Configuration\" (accessible from the Edit menu)."
]));
}
else
{
Entries.Add(new FaqEntry(
"Where are the downloads saved?",
[
"Your docker run command specifies the directory that content will be downloaded to. For instance, given the following docker run command, content will be downloaded to the $HOME/ofdl/data/ directory.",
"Do not change the download directory from the \"Download Behavior\" section in \"Configuration\" (accessible from the Edit menu) unless you know what you are doing. If the download directory is set to a value other than /data, content will not be downloaded where you expect and may not be accessible to you."
],
null,
DockerRunCommand));
}
Entries.Add(new FaqEntry(
"Why am I seeing a warning about CDM keys for DRM videos?",
[
"If you don't have your own CDM keys, a service will be used to download DRM videos. However, it is recommended that you use your own CDM keys for more reliable and faster downloads. The process is very easy if you use the bot on our Discord server.",
"See the documentation on CDM keys for more information:"
],
[
new FaqLink("https://docs.ofdl.tools/config/cdm/#discord-method",
"https://docs.ofdl.tools/config/cdm/#discord-method")
]));
if (isDocker)
{
Entries.Add(new FaqEntry(
"Why am I seeing a FFmpeg or FFprobe is missing error?",
[
"FFmpeg and FFprobe are required for OF DL to function. Both programs are included in the Docker image.",
"If you are seeing this error, the most likely cause is that you have manually defined a FFmpeg Path and/or FFprobe Path in your configuration. To fix this, erase the configured paths in the \"External\" section in \"Configuration\" (accessible from the Edit menu). Once you save the changes, OF DL will automatically detect the correct paths."
]));
}
else if (isWindows)
{
Entries.Add(new FaqEntry(
"Why am I seeing a FFmpeg or FFprobe is missing error?",
[
"FFmpeg and FFprobe are required for OF DL to function. Both programs are included with OF DL in the release .zip file. Make sure both files (ffmpeg.exe and ffprobe.exe) are in the same directory as OF DL.exe.",
"If you have already done this and still see an error, the most likely cause is that you have manually defined a FFmpeg Path and/or FFprobe Path in your configuration. To fix this, erase the configured paths in the \"External\" section in \"Configuration\" (accessible from the Edit menu). Once you save the changes, OF DL will automatically detect the correct paths."
]));
}
else
{
Entries.Add(new FaqEntry(
"Why am I seeing a FFmpeg or FFprobe is missing error?",
[
"FFmpeg and FFprobe are required for OF DL to function. Install both programs.",
"If the FFmpeg and FFprobe paths are not manually defined in the \"External\" section in \"Configuration\" (accessible from the Edit menu), OF DL searches for programs named ffmpeg and ffprobe in the same directory as OF DL and in the directories defined in the PATH environment variable."
]));
}
}
private async void OnLinkClick(object? sender, RoutedEventArgs e)
{
if (sender is not Button { CommandParameter: string url } || string.IsNullOrWhiteSpace(url))
{
return;
}
await OpenExternalUrlAsync(url);
}
private async Task OpenExternalUrlAsync(string url)
{
try
{
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
Process.Start(processStartInfo);
}
catch
{
await Task.CompletedTask;
}
}
}

View File

@ -48,14 +48,23 @@
<Grid RowDefinitions="Auto,Auto,*">
<Menu Grid.Row="0">
<MenuItem Header="_File">
<MenuItem Header="_Refresh Users" Command="{Binding RefreshUsersCommand}" />
<MenuItem Header="_Logout"
IsVisible="{Binding IsAuthenticated}"
Command="{Binding LogoutCommand}" />
<Separator />
<MenuItem Header="E_xit" Command="{Binding ExitApplicationCommand}" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Refresh Users" Command="{Binding RefreshUsersCommand}" />
<MenuItem Header="Edit _Config" Command="{Binding EditConfigCommand}" />
<MenuItem Header="_Configuration" Command="{Binding EditConfigCommand}" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_Join Discord" Click="OnJoinDiscordClick" />
<Separator />
<MenuItem Header="FA_Q" Click="OnFaqClick" />
<MenuItem Header="_Documentation" Click="OnDocumentationClick" />
<Separator />
<MenuItem Header="_About" Click="OnAboutClick" />
</MenuItem>
</Menu>
@ -111,7 +120,8 @@
<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="{DynamicResource TextPrimaryBrush}" Text="External" />
<TextBlock FontSize="16" FontWeight="Bold"
Foreground="{DynamicResource TextPrimaryBrush}" Text="External" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}"
@ -194,7 +204,8 @@
Text="Download Media Types" />
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Media Types" />
<TextBlock FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}" Text="Media Types" />
<ItemsControl ItemsSource="{Binding MediaTypeOptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -233,7 +244,8 @@
</StackPanel>
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Sources" />
<TextBlock FontWeight="SemiBold"
Foreground="{DynamicResource TextPrimaryBrush}" Text="Sources" />
<ItemsControl ItemsSource="{Binding MediaSourceOptions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@ -313,7 +325,7 @@
BorderThickness="1"
CornerRadius="10"
Content="?"
ToolTip.Tip="{Binding ViewModel.DownloadPathHelpText, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}" />
ToolTip.Tip="{Binding ViewModel.DownloadPathHelpText, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
@ -364,7 +376,8 @@
Content="?"
ToolTip.Tip="{Binding ViewModel.DrmVideoDurationMatchThresholdHelpText, RelativeSource={RelativeSource AncestorType=views:MainWindow}, FallbackValue=''}" />
</Grid>
<Grid Grid.Column="1" ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<Grid Grid.Column="1" ColumnDefinitions="*,Auto"
VerticalAlignment="Center">
<Slider Grid.Column="0"
Minimum="0"
Maximum="100"
@ -411,23 +424,23 @@
</StackPanel>
<Grid ColumnDefinitions="Auto,Auto,Auto"
HorizontalAlignment="Left">
<CheckBox Grid.Column="0"
Content="Enable"
VerticalAlignment="Center"
Margin="0,0,8,0"
IsChecked="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}" />
<ComboBox Grid.Column="1"
Width="140"
VerticalAlignment="Center"
Margin="0,0,8,0"
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}"
ItemsSource="{Binding DownloadDateSelectionField.EnumOptions, FallbackValue={x:Null}}"
SelectedItem="{Binding DownloadDateSelectionField.EnumValue, FallbackValue={x:Null}}" />
<DatePicker Grid.Column="2"
MinWidth="200"
VerticalAlignment="Center"
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}"
SelectedDate="{Binding CustomDateField.DateValue, FallbackValue={x:Null}}" />
<CheckBox Grid.Column="0"
Content="Enable"
VerticalAlignment="Center"
Margin="0,0,8,0"
IsChecked="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}" />
<ComboBox Grid.Column="1"
Width="140"
VerticalAlignment="Center"
Margin="0,0,8,0"
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}"
ItemsSource="{Binding DownloadDateSelectionField.EnumOptions, FallbackValue={x:Null}}"
SelectedItem="{Binding DownloadDateSelectionField.EnumValue, FallbackValue={x:Null}}" />
<DatePicker Grid.Column="2"
MinWidth="200"
VerticalAlignment="Center"
IsEnabled="{Binding DownloadOnlySpecificDatesField.BoolValue, FallbackValue=False}"
SelectedDate="{Binding CustomDateField.DateValue, FallbackValue={x:Null}}" />
</Grid>
</StackPanel>
<Grid IsVisible="{Binding HasRateLimitFields}"
@ -469,13 +482,13 @@
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<CheckBox Content="Enable"
IsChecked="{Binding LimitDownloadRateField.BoolValue, FallbackValue=False}" />
<NumericUpDown MinWidth="120"
IsEnabled="{Binding LimitDownloadRateField.BoolValue, FallbackValue=False}"
Value="{Binding DownloadLimitInMbPerSecField.NumericValue, FallbackValue=0}"
Increment="1"
FormatString="N0" />
<CheckBox Content="Enable"
IsChecked="{Binding LimitDownloadRateField.BoolValue, FallbackValue=False}" />
<NumericUpDown MinWidth="120"
IsEnabled="{Binding LimitDownloadRateField.BoolValue, FallbackValue=False}"
Value="{Binding DownloadLimitInMbPerSecField.NumericValue, FallbackValue=0}"
Increment="1"
FormatString="N0" />
<TextBlock Text="Mbps"
Foreground="{DynamicResource TextSecondaryBrush}"
VerticalAlignment="Center" />
@ -516,16 +529,16 @@
Content="?"
ToolTip.Tip="{Binding FolderStructureHelpText}" />
</Grid>
<StackPanel Grid.Column="1" Spacing="6">
<CheckBox Content="Paid Posts"
IsChecked="{Binding FolderPerPaidPostField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Free Posts"
IsChecked="{Binding FolderPerPostField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Paid Messages"
IsChecked="{Binding FolderPerPaidMessageField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Free Messages"
IsChecked="{Binding FolderPerMessageField.BoolValue, FallbackValue=False}" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="6">
<CheckBox Content="Paid Posts"
IsChecked="{Binding FolderPerPaidPostField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Free Posts"
IsChecked="{Binding FolderPerPostField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Paid Messages"
IsChecked="{Binding FolderPerPaidMessageField.BoolValue, FallbackValue=False}" />
<CheckBox Content="Free Messages"
IsChecked="{Binding FolderPerMessageField.BoolValue, FallbackValue=False}" />
</StackPanel>
</Grid>
<ItemsControl ItemsSource="{Binding Fields}">
<ItemsControl.ItemsPanel>
@ -865,7 +878,8 @@
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Users" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Users" />
<CheckBox IsThreeState="True"
IsChecked="{Binding AllUsersSelected}"
Content="{Binding SelectedUsersSummary}"
@ -903,7 +917,8 @@
Padding="10"
Classes="surface">
<Grid RowDefinitions="Auto,*">
<TextBlock Grid.Row="0" FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" 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}"
@ -971,7 +986,8 @@
Text="{Binding DialogTitle}" />
<StackPanel Spacing="6">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Username" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Username" />
<Grid ColumnDefinitions="*,Auto">
<AutoCompleteBox Grid.Column="0"
Text="{Binding Username}"
@ -987,10 +1003,11 @@
<TextBlock FontWeight="SemiBold"
Foreground="{DynamicResource TextSecondaryBrush}"
Text="Filename Formats (leave blank to use global defaults)" />
Text="File Name Formats (leave blank to use global defaults)" />
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Post Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Paid Post File Name Format" />
<Grid ClipToBounds="True">
<TextBox x:Name="PaidPostFileNameFormatTextBox"
Classes="fileNameOverlayInput"
@ -1030,7 +1047,8 @@
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Post Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Post File Name Format" />
<Grid ClipToBounds="True">
<TextBox x:Name="PostFileNameFormatTextBox"
Classes="fileNameOverlayInput"
@ -1070,7 +1088,8 @@
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Paid Message Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Paid Message File Name Format" />
<Grid ClipToBounds="True">
<TextBox x:Name="PaidMessageFileNameFormatTextBox"
Classes="fileNameOverlayInput"
@ -1110,7 +1129,8 @@
</StackPanel>
<StackPanel Spacing="8">
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}" Text="Message Filename Format" />
<TextBlock FontWeight="SemiBold" Foreground="{DynamicResource TextPrimaryBrush}"
Text="Message File Name Format" />
<Grid ClipToBounds="True">
<TextBox x:Name="MessageFileNameFormatTextBox"
Classes="fileNameOverlayInput"
@ -1163,5 +1183,3 @@
</Grid>
</Grid>
</Window>

View File

@ -1,3 +1,4 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -9,6 +10,8 @@ namespace OF_DL.Gui.Views;
public partial class MainWindow : Window
{
private const string DiscordInviteUrl = "https://discord.com/invite/6bUW8EJ53j";
private const string DocumentationUrl = "https://docs.ofdl.tools/";
private bool _hasInitialized;
public MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
@ -129,6 +132,43 @@ public partial class MainWindow : Window
vm.SetDownloadPath(selectedFolder.Name);
}
private async void OnJoinDiscordClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(DiscordInviteUrl);
private async void OnDocumentationClick(object? sender, RoutedEventArgs e) =>
await OpenExternalUrlAsync(DocumentationUrl);
private void OnFaqClick(object? sender, RoutedEventArgs e)
{
FaqWindow faqWindow = new();
faqWindow.Show(this);
}
private void OnAboutClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)
{
return;
}
AboutWindow aboutWindow = new(vm.ProgramVersion, vm.FfmpegVersion, vm.FfprobeVersion);
aboutWindow.Show(this);
}
private async Task OpenExternalUrlAsync(string url)
{
try
{
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
Process.Start(processStartInfo);
}
catch
{
await Task.CompletedTask;
}
}
private void OnModalOverlayClicked(object? sender, PointerPressedEventArgs e)
{
// Only handle clicks on the overlay itself (the Grid background)

View File

@ -13,6 +13,13 @@ the authorization process has finished. If the auth info is correct then you sho
then a message in red text will appear `Auth failed, please check the values in auth.json are correct, press any key to exit.`
This means you need to go back and fill in the `auth.json` file again, this will usually indicate that your `user-agent` has changed or you need to re-copy your `sess` value.
In GUI mode, the **Help** menu includes:
- **Join Discord** (opens the OF-DL Discord invite)
- **FAQ** (opens the FAQ window; content coming soon)
- **Documentation** (opens https://docs.ofdl.tools/)
- **About** (shows version details and project/license links)
If you're logged in successfully then you will be greeted with a selection prompt. To navigate the menu the can use the ↑ & ↓ arrows and press `enter` to choose that option.
![CLI main menu](/img/cli_menu.png)