Update web link handling to copy links to clipboard when click inside docker
This commit is contained in:
parent
4d3ae0e19a
commit
2dcb9a3753
@ -24,6 +24,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`, `AboutWindow`, `FaqWindow`, MVVM view models, and GUI event handlers).
|
||||
- `OF DL.Gui/Helpers/` contains GUI-specific utility helpers (for example, Docker-aware web-link behavior shared across windows).
|
||||
- `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.
|
||||
|
||||
@ -2,6 +2,10 @@ namespace OF_DL.Helpers;
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const string DiscordInviteUrl = "https://discord.com/invite/6bUW8EJ53j";
|
||||
|
||||
public const string DocumentationUrl = "https://docs.ofdl.tools/";
|
||||
|
||||
public const string ApiUrl = "https://onlyfans.com/api2/v2";
|
||||
|
||||
public const int ApiPageSize = 50;
|
||||
|
||||
92
OF DL.Gui/Helpers/WebLinkHelper.cs
Normal file
92
OF DL.Gui/Helpers/WebLinkHelper.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Controls;
|
||||
using OF_DL.Helpers;
|
||||
using Serilog;
|
||||
|
||||
namespace OF_DL.Gui.Helpers;
|
||||
|
||||
public static class WebLinkHelper
|
||||
{
|
||||
private const string CopiedToClipboardMessage = "Copied to clipboard";
|
||||
|
||||
public static async Task OpenOrCopyAsync(
|
||||
Window owner,
|
||||
string url,
|
||||
Control? toolTipTarget = null,
|
||||
Func<string, Task>? dockerFeedbackAsync = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (EnvironmentHelper.IsRunningInDocker())
|
||||
{
|
||||
TopLevel? topLevel = TopLevel.GetTopLevel(owner);
|
||||
if (topLevel?.Clipboard != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await topLevel.Clipboard.SetTextAsync(url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (dockerFeedbackAsync != null)
|
||||
{
|
||||
await dockerFeedbackAsync(CopiedToClipboardMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolTipTarget != null)
|
||||
{
|
||||
await ShowTemporaryTooltipAsync(toolTipTarget, CopiedToClipboardMessage);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error("Failed to copy URL to clipboard. {ErrorMessage}", e.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OpenExternalUrl(url);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Failed to open external URL. {ErrorMessage}", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ShowTemporaryTooltipAsync(
|
||||
Control target,
|
||||
string message,
|
||||
int durationMilliseconds = 1500)
|
||||
{
|
||||
object? originalTip = ToolTip.GetTip(target);
|
||||
|
||||
ToolTip.SetTip(target, message);
|
||||
ToolTip.SetIsOpen(target, true);
|
||||
|
||||
await Task.Delay(durationMilliseconds);
|
||||
|
||||
ToolTip.SetIsOpen(target, false);
|
||||
ToolTip.SetTip(target, originalTip);
|
||||
}
|
||||
|
||||
private static void OpenExternalUrl(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
|
||||
Process.Start(processStartInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore browser launch failures to preserve prior behavior.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform;
|
||||
using OF_DL.Gui.Helpers;
|
||||
using Serilog;
|
||||
|
||||
namespace OF_DL.Gui.Views;
|
||||
|
||||
@ -37,26 +38,39 @@ public partial class AboutWindow : Window
|
||||
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 async Task OpenExternalUrlAsync(string url)
|
||||
private async void OnOpenSourceCodeClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
|
||||
|
||||
Process.Start(processStartInfo);
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, SourceCodeUrl, sender as Control);
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnOpenFfmpegLicenseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, FfmpegLicenseUrl, sender as Control);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnOpenFfprobeLicenseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, FfprobeLicenseUrl, sender as Control);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,18 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform;
|
||||
using OF_DL.Gui.Helpers;
|
||||
using OF_DL.Helpers;
|
||||
using Serilog;
|
||||
|
||||
namespace OF_DL.Gui.Views;
|
||||
|
||||
public class FaqLink
|
||||
public class FaqLink(string label, string url)
|
||||
{
|
||||
public FaqLink(string label, string url)
|
||||
{
|
||||
Label = label;
|
||||
Url = url;
|
||||
}
|
||||
public string Label { get; } = label;
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public string Url { get; }
|
||||
public string Url { get; } = url;
|
||||
}
|
||||
|
||||
public class FaqEntry
|
||||
@ -136,26 +131,19 @@ public partial class FaqWindow : Window
|
||||
}
|
||||
|
||||
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 };
|
||||
if (sender is not Button { CommandParameter: string url } || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(processStartInfo);
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, url, sender as Control);
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,6 +144,21 @@
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.copyToast">
|
||||
<Setter Property="Background" Value="{DynamicResource PrimaryButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SurfaceBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
<Setter Property="BoxShadow" Value="0 6 12 -4 #33000000, 0 2 4 -2 #26000000" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.18" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
@ -1491,5 +1506,20 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="CopyToastBorder"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Classes="copyToast"
|
||||
IsVisible="False"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,20,20"
|
||||
ZIndex="2000">
|
||||
<TextBlock x:Name="CopyToastTextBlock"
|
||||
Foreground="{DynamicResource PrimaryButtonForegroundBrush}"
|
||||
FontWeight="SemiBold"
|
||||
Text="Copied to clipboard" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Specialized;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
@ -8,19 +7,20 @@ using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Threading;
|
||||
using OF_DL.Helpers;
|
||||
using OF_DL.Gui.Helpers;
|
||||
using OF_DL.Gui.ViewModels;
|
||||
using Serilog;
|
||||
|
||||
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;
|
||||
private bool _activityLogAutoScroll = true;
|
||||
private bool _activityLogUserInteracted;
|
||||
private bool _isActivityLogProgrammaticScroll;
|
||||
private ScrollViewer? _activityLogScrollViewer;
|
||||
private CancellationTokenSource? _copyToastCancellationTokenSource;
|
||||
public MainWindowViewModel? ViewModel => DataContext as MainWindowViewModel;
|
||||
|
||||
public MainWindow()
|
||||
@ -68,6 +68,10 @@ public partial class MainWindow : Window
|
||||
|
||||
ActivityLogListBox.PointerWheelChanged -= OnActivityLogPointerInteracted;
|
||||
ActivityLogListBox.PointerPressed -= OnActivityLogPointerInteracted;
|
||||
|
||||
_copyToastCancellationTokenSource?.Cancel();
|
||||
_copyToastCancellationTokenSource?.Dispose();
|
||||
_copyToastCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private async void OnBrowseFfmpegPathClick(object? sender, RoutedEventArgs e)
|
||||
@ -166,11 +170,31 @@ public partial class MainWindow : Window
|
||||
vm.SetDownloadPath(selectedFolder.Name);
|
||||
}
|
||||
|
||||
private async void OnJoinDiscordClick(object? sender, RoutedEventArgs e) =>
|
||||
await OpenExternalUrlAsync(DiscordInviteUrl);
|
||||
private async void OnJoinDiscordClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, Constants.DiscordInviteUrl,
|
||||
dockerFeedbackAsync: ShowCopyToastAsync);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDocumentationClick(object? sender, RoutedEventArgs e) =>
|
||||
await OpenExternalUrlAsync(DocumentationUrl);
|
||||
private async void OnDocumentationClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebLinkHelper.OpenOrCopyAsync(this, Constants.DocumentationUrl,
|
||||
dockerFeedbackAsync: ShowCopyToastAsync);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception, "Failed to handle link click event. {ErrorMessage}", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFaqClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
@ -192,17 +216,45 @@ public partial class MainWindow : Window
|
||||
aboutWindow.Show(this);
|
||||
}
|
||||
|
||||
private async Task OpenExternalUrlAsync(string url)
|
||||
private async Task ShowCopyToastAsync(string message)
|
||||
{
|
||||
if (_copyToastCancellationTokenSource != null)
|
||||
{
|
||||
await _copyToastCancellationTokenSource.CancelAsync();
|
||||
_copyToastCancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
CancellationTokenSource cancellationTokenSource = new();
|
||||
_copyToastCancellationTokenSource = cancellationTokenSource;
|
||||
|
||||
CopyToastTextBlock.Text = message;
|
||||
CopyToastBorder.Opacity = 0;
|
||||
CopyToastBorder.IsVisible = true;
|
||||
CopyToastBorder.Opacity = 1;
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processStartInfo = new(url) { UseShellExecute = true };
|
||||
|
||||
Process.Start(processStartInfo);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationTokenSource.Token);
|
||||
}
|
||||
catch
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
CopyToastBorder.Opacity = 0;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(180), cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
CopyToastBorder.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,7 +307,8 @@ public partial class MainWindow : Window
|
||||
Dispatcher.UIThread.Post(ScrollActivityLogToBottom);
|
||||
}
|
||||
|
||||
private void OnActivityLogPointerInteracted(object? sender, PointerEventArgs e) => _activityLogUserInteracted = true;
|
||||
private void OnActivityLogPointerInteracted(object? sender, PointerEventArgs e) =>
|
||||
_activityLogUserInteracted = true;
|
||||
|
||||
private void OnActivityLogScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
|
||||
@ -1,23 +1,27 @@
|
||||
# Running the Program
|
||||
|
||||
Once you are happy you have filled everything in [auth.json](/config/auth) correctly, you can double click OF-DL.exe and you should see a command prompt window appear, it should look something like this:
|
||||
Once you are happy you have filled everything in [auth.json](/config/auth) correctly, you can double click OF-DL.exe and
|
||||
you should see a command prompt window appear, it should look something like this:
|
||||
|
||||

|
||||
|
||||
It should locate `config.conf`, `rules.json`, FFmpeg, and FFprobe successfully. If anything doesn't get located
|
||||
successfully, then make sure the files exist or the path is correct.
|
||||
|
||||
OF-DL will open a new window, if needed, to allow you to log into your OnlyFans account. The window will automatically close once
|
||||
OF-DL will open a new window, if needed, to allow you to log into your OnlyFans account. The window will automatically
|
||||
close once
|
||||
the authorization process has finished. If the auth info is correct then you should see a message in green text
|
||||
`Logged In successfully as {Your Username} {Your User Id}`. However, if the authorization has failed,
|
||||
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.
|
||||
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)
|
||||
- **Join Discord** (opens the OF-DL Discord invite; in Docker, copies the link to clipboard)
|
||||
- **FAQ** (opens the FAQ window; content coming soon)
|
||||
- **Documentation** (opens https://docs.ofdl.tools/)
|
||||
- **Documentation** (opens https://docs.ofdl.tools/; in Docker, copies the link to clipboard)
|
||||
- **About** (shows version details and project/license links)
|
||||
|
||||
In GUI mode, the main download screen includes:
|
||||
@ -33,30 +37,40 @@ For **Download Single Post/Message** in GUI:
|
||||
find the message, click `...`, and choose **Copy link to message**.
|
||||
- Other message types cannot be downloaded individually by URL; scrape all messages for that creator instead.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
The `Select All` option will go through every account you are currently subscribed to and grab all of the media from the users.
|
||||
The `Select All` option will go through every account you are currently subscribed to and grab all of the media from the
|
||||
users.
|
||||
|
||||
The `List` option will show you all the lists you have created on OnlyFans and you can then select 1 or more lists to download the content of the users within those lists.
|
||||
The `List` option will show you all the lists you have created on OnlyFans and you can then select 1 or more lists to
|
||||
download the content of the users within those lists.
|
||||
|
||||
The `Custom` option allows you to select 1 or more accounts you want to scrape media from so if you only want to get media from a select number of accounts then you can do that.
|
||||
To navigate the menu the can use the ↑ & ↓ arrows. You can also press keys A-Z on the keyboard whilst in the menu to easily navigate the menu and for example
|
||||
pressing the letter 'c' on the keyboard will highlight the first user in the list whose username starts with the letter 'c'. To select/deselect an account,
|
||||
The `Custom` option allows you to select 1 or more accounts you want to scrape media from so if you only want to get
|
||||
media from a select number of accounts then you can do that.
|
||||
To navigate the menu the can use the ↑ & ↓ arrows. You can also press keys A-Z on the keyboard whilst in the menu to
|
||||
easily navigate the menu and for example
|
||||
pressing the letter 'c' on the keyboard will highlight the first user in the list whose username starts with the
|
||||
letter 'c'. To select/deselect an account,
|
||||
press the space key, and after you are happy with your selection(s), press the enter key to start downloading.
|
||||
|
||||
The `Download Single Post` allows you to download a post from a URL, to get this URL go to any post and press the 3 dots, Copy link to post.
|
||||
The `Download Single Post` allows you to download a post from a URL, to get this URL go to any post and press the 3
|
||||
dots, Copy link to post.
|
||||
|
||||
The `Download Single Message` allows you to download a message from a URL, to get this URL go to any message in the **purchased tab** and press the 3 dots, Copy link to message.
|
||||
The `Download Single Message` allows you to download a message from a URL, to get this URL go to any message in the *
|
||||
*purchased tab** and press the 3 dots, Copy link to message.
|
||||
|
||||
The `Download Purchased Tab` option will download all the media from the purchased tab in OnlyFans.
|
||||
|
||||
The `Edit config.json` option allows you to change the config from within the program.
|
||||
|
||||
The `Change logging level` option allows you to change the logging level that the program uses when writing logs to files in the `logs` folder.
|
||||
The `Change logging level` option allows you to change the logging level that the program uses when writing logs to
|
||||
files in the `logs` folder.
|
||||
|
||||
The `Logout and Exit` option allows you to remove your authentication from OF-DL. This is useful if you use multiple OnlyFans accounts.
|
||||
The `Logout and Exit` option allows you to remove your authentication from OF-DL. This is useful if you use multiple
|
||||
OnlyFans accounts.
|
||||
|
||||
After you have made your selection the content should start downloading. Content is downloaded in this order:
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user