Add a download single post/message option to the GUI
This commit is contained in:
parent
ac4061f1ca
commit
d662d9be4d
@ -27,7 +27,26 @@ public partial class MainWindowViewModel(
|
|||||||
IStartupService startupService,
|
IStartupService startupService,
|
||||||
IDownloadOrchestrationService downloadOrchestrationService) : ViewModelBase
|
IDownloadOrchestrationService downloadOrchestrationService) : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private enum SingleDownloadType
|
||||||
|
{
|
||||||
|
Post,
|
||||||
|
PaidMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct SingleDownloadRequest(
|
||||||
|
SingleDownloadType Type,
|
||||||
|
long ContentId,
|
||||||
|
string Username,
|
||||||
|
long? UserId);
|
||||||
|
|
||||||
private const string UnknownToolVersion = "Not detected";
|
private const string UnknownToolVersion = "Not detected";
|
||||||
|
private static readonly Regex s_singlePostUrlRegex = new(
|
||||||
|
@"^https://onlyfans\.com/(?<postId>\d+)/(?<username>[A-Za-z0-9_.-]+)/?$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private static readonly Regex s_singlePaidMessageUrlRegex = new(
|
||||||
|
@"^https://onlyfans\.com/my/chats/chat/(?<userId>\d+)/?\?firstId=(?<messageId>\d+)$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
private static readonly string s_defaultDownloadPath = Path.GetFullPath(
|
private static readonly string s_defaultDownloadPath = Path.GetFullPath(
|
||||||
Path.Combine(Directory.GetCurrentDirectory(), "__user_data__", "sites", "OnlyFans"));
|
Path.Combine(Directory.GetCurrentDirectory(), "__user_data__", "sites", "OnlyFans"));
|
||||||
@ -297,6 +316,19 @@ public partial class MainWindowViewModel(
|
|||||||
|
|
||||||
[ObservableProperty] private string _downloadProgressDescription = string.Empty;
|
[ObservableProperty] private string _downloadProgressDescription = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(OpenSinglePostOrMessageModalCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SubmitSinglePostOrMessageCommand))]
|
||||||
|
private bool _isSinglePostOrMessageModalOpen;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(SubmitSinglePostOrMessageCommand))]
|
||||||
|
private string _singlePostOrMessageUrl = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasSinglePostOrMessageUrlError))]
|
||||||
|
private string _singlePostOrMessageUrlError = string.Empty;
|
||||||
|
|
||||||
public bool IsLoadingScreen => CurrentScreen == AppScreen.Loading;
|
public bool IsLoadingScreen => CurrentScreen == AppScreen.Loading;
|
||||||
|
|
||||||
public bool IsConfigScreen => CurrentScreen == AppScreen.Config;
|
public bool IsConfigScreen => CurrentScreen == AppScreen.Config;
|
||||||
@ -317,6 +349,8 @@ public partial class MainWindowViewModel(
|
|||||||
|
|
||||||
public bool HasMediaSourcesError => !string.IsNullOrWhiteSpace(MediaSourcesError);
|
public bool HasMediaSourcesError => !string.IsNullOrWhiteSpace(MediaSourcesError);
|
||||||
|
|
||||||
|
public bool HasSinglePostOrMessageUrlError => !string.IsNullOrWhiteSpace(SinglePostOrMessageUrlError);
|
||||||
|
|
||||||
public string FfmpegPathDisplay =>
|
public string FfmpegPathDisplay =>
|
||||||
HidePrivateInfo && !string.IsNullOrWhiteSpace(FfmpegPath) ? "[Hidden for Privacy]" : FfmpegPath;
|
HidePrivateInfo && !string.IsNullOrWhiteSpace(FfmpegPath) ? "[Hidden for Privacy]" : FfmpegPath;
|
||||||
|
|
||||||
@ -688,6 +722,36 @@ public partial class MainWindowViewModel(
|
|||||||
[RelayCommand(CanExecute = nameof(CanDownloadPurchasedTab))]
|
[RelayCommand(CanExecute = nameof(CanDownloadPurchasedTab))]
|
||||||
private async Task DownloadPurchasedTabAsync() => await RunDownloadAsync(true);
|
private async Task DownloadPurchasedTabAsync() => await RunDownloadAsync(true);
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenSinglePostOrMessageModal))]
|
||||||
|
private void OpenSinglePostOrMessageModal()
|
||||||
|
{
|
||||||
|
SinglePostOrMessageUrl = string.Empty;
|
||||||
|
SinglePostOrMessageUrlError = string.Empty;
|
||||||
|
IsSinglePostOrMessageModalOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanSubmitSinglePostOrMessage))]
|
||||||
|
private async Task SubmitSinglePostOrMessageAsync()
|
||||||
|
{
|
||||||
|
if (!TryParseSinglePostOrMessageUrl(SinglePostOrMessageUrl, out SingleDownloadRequest request,
|
||||||
|
out string validationError))
|
||||||
|
{
|
||||||
|
SinglePostOrMessageUrlError = validationError;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsSinglePostOrMessageModalOpen = false;
|
||||||
|
SinglePostOrMessageUrlError = string.Empty;
|
||||||
|
await RunSinglePostOrMessageDownloadAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CancelSinglePostOrMessage()
|
||||||
|
{
|
||||||
|
SinglePostOrMessageUrlError = string.Empty;
|
||||||
|
IsSinglePostOrMessageModalOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanStopWork))]
|
[RelayCommand(CanExecute = nameof(CanStopWork))]
|
||||||
private void StopWork()
|
private void StopWork()
|
||||||
{
|
{
|
||||||
@ -814,6 +878,16 @@ public partial class MainWindowViewModel(
|
|||||||
_allUsers.Count > 0 &&
|
_allUsers.Count > 0 &&
|
||||||
!IsDownloading;
|
!IsDownloading;
|
||||||
|
|
||||||
|
private bool CanOpenSinglePostOrMessageModal() =>
|
||||||
|
CurrentScreen == AppScreen.UserSelection &&
|
||||||
|
!IsDownloading &&
|
||||||
|
!IsSinglePostOrMessageModalOpen;
|
||||||
|
|
||||||
|
private bool CanSubmitSinglePostOrMessage() =>
|
||||||
|
IsSinglePostOrMessageModalOpen &&
|
||||||
|
!IsDownloading &&
|
||||||
|
!string.IsNullOrWhiteSpace(SinglePostOrMessageUrl);
|
||||||
|
|
||||||
private bool CanStopWork() => IsDownloading;
|
private bool CanStopWork() => IsDownloading;
|
||||||
|
|
||||||
private bool CanRefreshUsers() =>
|
private bool CanRefreshUsers() =>
|
||||||
@ -842,6 +916,8 @@ public partial class MainWindowViewModel(
|
|||||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
LogoutCommand.NotifyCanExecuteChanged();
|
LogoutCommand.NotifyCanExecuteChanged();
|
||||||
EditConfigCommand.NotifyCanExecuteChanged();
|
EditConfigCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
|
||||||
|
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsDownloadingChanged(bool value)
|
partial void OnIsDownloadingChanged(bool value)
|
||||||
@ -853,6 +929,8 @@ public partial class MainWindowViewModel(
|
|||||||
RefreshUsersCommand.NotifyCanExecuteChanged();
|
RefreshUsersCommand.NotifyCanExecuteChanged();
|
||||||
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
RefreshIgnoredUsersListsCommand.NotifyCanExecuteChanged();
|
||||||
LogoutCommand.NotifyCanExecuteChanged();
|
LogoutCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
|
||||||
|
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsAuthenticatedChanged(bool value)
|
partial void OnIsAuthenticatedChanged(bool value)
|
||||||
@ -873,6 +951,11 @@ public partial class MainWindowViewModel(
|
|||||||
_ = SelectUsersFromListAsync();
|
_ = SelectUsersFromListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnSinglePostOrMessageUrlChanged(string value)
|
||||||
|
{
|
||||||
|
SinglePostOrMessageUrlError = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnFfmpegPathChanged(string value)
|
partial void OnFfmpegPathChanged(string value)
|
||||||
{
|
{
|
||||||
if (value != "[Hidden for Privacy]")
|
if (value != "[Hidden for Privacy]")
|
||||||
@ -933,6 +1016,163 @@ public partial class MainWindowViewModel(
|
|||||||
await EnsureAuthenticationAndLoadUsersAsync();
|
await EnsureAuthenticationAndLoadUsersAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RunSinglePostOrMessageDownloadAsync(SingleDownloadRequest request)
|
||||||
|
{
|
||||||
|
if (_allUsers.Count == 0)
|
||||||
|
{
|
||||||
|
StatusMessage = "No users are loaded. Refresh users and retry.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsDownloading = true;
|
||||||
|
_workCancellationSource?.Dispose();
|
||||||
|
_workCancellationSource = new CancellationTokenSource();
|
||||||
|
DownloadSelectedCommand.NotifyCanExecuteChanged();
|
||||||
|
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
|
||||||
|
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
|
||||||
|
StopWorkCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
|
DateTime start = DateTime.Now;
|
||||||
|
string label = request.Type == SingleDownloadType.Post ? "single post" : "single paid message";
|
||||||
|
AppendLog($"Starting {label} download from URL.");
|
||||||
|
StatusMessage = $"Starting {label} download...";
|
||||||
|
|
||||||
|
StartDownloadProgress($"Initializing {label} download...", 0, false);
|
||||||
|
|
||||||
|
CancellationTokenSource cancellationSource = _workCancellationSource;
|
||||||
|
AvaloniaDownloadEventHandler eventHandler = new(
|
||||||
|
AppendLog,
|
||||||
|
UpdateProgressStatus,
|
||||||
|
StartDownloadProgress,
|
||||||
|
IncrementDownloadProgress,
|
||||||
|
StopDownloadProgress,
|
||||||
|
() => cancellationSource.IsCancellationRequested,
|
||||||
|
cancellationSource.Token);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.Type == SingleDownloadType.Post)
|
||||||
|
{
|
||||||
|
if (!_allUsers.TryGetValue(request.Username, out long userId))
|
||||||
|
{
|
||||||
|
StatusMessage =
|
||||||
|
$"Creator '{request.Username}' is not in loaded users. Refresh users and ensure you are subscribed.";
|
||||||
|
AppendLog(StatusMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string path = downloadOrchestrationService.ResolveDownloadPath(request.Username);
|
||||||
|
await downloadOrchestrationService.PrepareUserFolderAsync(request.Username, userId, path);
|
||||||
|
|
||||||
|
await downloadOrchestrationService.DownloadSinglePostAsync(
|
||||||
|
request.Username,
|
||||||
|
request.ContentId,
|
||||||
|
path,
|
||||||
|
_allUsers,
|
||||||
|
_startupResult.ClientIdBlobMissing,
|
||||||
|
_startupResult.DevicePrivateKeyMissing,
|
||||||
|
eventHandler);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long userId = request.UserId ?? 0;
|
||||||
|
string? resolvedUsername = await downloadOrchestrationService.ResolveUsernameAsync(userId);
|
||||||
|
if (string.IsNullOrWhiteSpace(resolvedUsername))
|
||||||
|
{
|
||||||
|
StatusMessage = $"Could not resolve username for user ID {userId}.";
|
||||||
|
AppendLog(StatusMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, long> usersForDownload = new(_allUsers, StringComparer.Ordinal);
|
||||||
|
if (!usersForDownload.ContainsKey(resolvedUsername))
|
||||||
|
{
|
||||||
|
usersForDownload[resolvedUsername] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
string path = downloadOrchestrationService.ResolveDownloadPath(resolvedUsername);
|
||||||
|
await downloadOrchestrationService.PrepareUserFolderAsync(resolvedUsername, userId, path);
|
||||||
|
|
||||||
|
await downloadOrchestrationService.DownloadSinglePaidMessageAsync(
|
||||||
|
resolvedUsername,
|
||||||
|
request.ContentId,
|
||||||
|
path,
|
||||||
|
usersForDownload,
|
||||||
|
_startupResult.ClientIdBlobMissing,
|
||||||
|
_startupResult.DevicePrivateKeyMissing,
|
||||||
|
eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowIfStopRequested();
|
||||||
|
eventHandler.OnScrapeComplete(DateTime.Now - start);
|
||||||
|
StatusMessage = request.Type == SingleDownloadType.Post
|
||||||
|
? "Single post download completed."
|
||||||
|
: "Single paid message download completed.";
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
StatusMessage = "Operation canceled.";
|
||||||
|
AppendLog("Operation canceled.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppendLog($"Single item download failed: {ex.Message}");
|
||||||
|
StatusMessage = "Single item download failed. Check logs.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsDownloading = false;
|
||||||
|
_workCancellationSource?.Dispose();
|
||||||
|
_workCancellationSource = null;
|
||||||
|
StopDownloadProgress();
|
||||||
|
DownloadSelectedCommand.NotifyCanExecuteChanged();
|
||||||
|
DownloadPurchasedTabCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenSinglePostOrMessageModalCommand.NotifyCanExecuteChanged();
|
||||||
|
SubmitSinglePostOrMessageCommand.NotifyCanExecuteChanged();
|
||||||
|
StopWorkCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseSinglePostOrMessageUrl(
|
||||||
|
string url,
|
||||||
|
out SingleDownloadRequest request,
|
||||||
|
out string validationError)
|
||||||
|
{
|
||||||
|
request = default;
|
||||||
|
validationError = string.Empty;
|
||||||
|
|
||||||
|
string trimmedUrl = url.Trim();
|
||||||
|
Match postMatch = s_singlePostUrlRegex.Match(trimmedUrl);
|
||||||
|
if (postMatch.Success &&
|
||||||
|
long.TryParse(postMatch.Groups["postId"].Value, out long postId) &&
|
||||||
|
!string.IsNullOrWhiteSpace(postMatch.Groups["username"].Value))
|
||||||
|
{
|
||||||
|
request = new SingleDownloadRequest(
|
||||||
|
SingleDownloadType.Post,
|
||||||
|
postId,
|
||||||
|
postMatch.Groups["username"].Value,
|
||||||
|
null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Match paidMessageMatch = s_singlePaidMessageUrlRegex.Match(trimmedUrl);
|
||||||
|
if (paidMessageMatch.Success &&
|
||||||
|
long.TryParse(paidMessageMatch.Groups["messageId"].Value, out long messageId) &&
|
||||||
|
long.TryParse(paidMessageMatch.Groups["userId"].Value, out long userId))
|
||||||
|
{
|
||||||
|
request = new SingleDownloadRequest(
|
||||||
|
SingleDownloadType.PaidMessage,
|
||||||
|
messageId,
|
||||||
|
string.Empty,
|
||||||
|
userId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError = "Please enter a valid post URL or paid message URL.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ApplyThemeFromConfigFileIfAvailable()
|
private static void ApplyThemeFromConfigFileIfAvailable()
|
||||||
{
|
{
|
||||||
const string configPath = "config.conf";
|
const string configPath = "config.conf";
|
||||||
|
|||||||
@ -1008,6 +1008,9 @@
|
|||||||
<Button Content="Download Purchased Tab"
|
<Button Content="Download Purchased Tab"
|
||||||
Classes="secondary"
|
Classes="secondary"
|
||||||
Command="{Binding DownloadPurchasedTabCommand}" />
|
Command="{Binding DownloadPurchasedTabCommand}" />
|
||||||
|
<Button Content="Download Single Post/Message"
|
||||||
|
Classes="secondary"
|
||||||
|
Command="{Binding OpenSinglePostOrMessageModalCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
IsVisible="{Binding IsDownloading}"
|
IsVisible="{Binding IsDownloading}"
|
||||||
@ -1349,5 +1352,95 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="0" Grid.RowSpan="3"
|
||||||
|
IsVisible="{Binding IsSinglePostOrMessageModalOpen}"
|
||||||
|
Background="{DynamicResource OverlayBackgroundBrush}"
|
||||||
|
ZIndex="1001"
|
||||||
|
PointerPressed="OnModalOverlayClicked">
|
||||||
|
<Border Background="{DynamicResource ModalBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource ModalBorderBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="16"
|
||||||
|
Padding="32"
|
||||||
|
Width="760"
|
||||||
|
MaxHeight="700"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
BoxShadow="0 20 25 -5 #19000000, 0 10 10 -5 #0F000000"
|
||||||
|
PointerPressed="OnModalContentClicked">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="18" Margin="8,4,8,4">
|
||||||
|
<TextBlock FontSize="20"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||||
|
Text="Download Single Post/Message" />
|
||||||
|
|
||||||
|
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Enter a post URL or paid message URL to download only that item." />
|
||||||
|
|
||||||
|
<Border Padding="16"
|
||||||
|
CornerRadius="10"
|
||||||
|
Background="{DynamicResource PreviewBackgroundBrush}"
|
||||||
|
BorderBrush="{DynamicResource PreviewBorderBrush}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock FontSize="15"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||||
|
Text="How to find a URL" />
|
||||||
|
|
||||||
|
<!-- Post Instructions -->
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||||
|
Text="Posts" />
|
||||||
|
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Click the ... menu on any post → 'Copy link to post'" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Message Instructions -->
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||||
|
Text="Messages (Paid Only)" />
|
||||||
|
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Go to main timeline → Purchased tab → Find message → Click ... menu → 'Copy link to message'" />
|
||||||
|
<TextBlock Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
FontStyle="Italic"
|
||||||
|
FontSize="12"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="Note: Only unlocked PPV messages can be downloaded individually. For other messages, download all messages from the creator." />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||||
|
Text="Post or Message URL" />
|
||||||
|
<TextBox Text="{Binding SinglePostOrMessageUrl}"
|
||||||
|
Watermark="https://onlyfans.com/... or https://onlyfans.com/my/chats/chat/.../?firstId=..." />
|
||||||
|
<TextBlock IsVisible="{Binding HasSinglePostOrMessageUrlError}"
|
||||||
|
Foreground="{DynamicResource ErrorTextBrush}"
|
||||||
|
Text="{Binding SinglePostOrMessageUrlError}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Cancel"
|
||||||
|
Classes="secondary"
|
||||||
|
Command="{Binding CancelSinglePostOrMessageCommand}" />
|
||||||
|
<Button Content="Download"
|
||||||
|
Classes="primary"
|
||||||
|
Command="{Binding SubmitSinglePostOrMessageCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@ -183,8 +183,9 @@ public partial class MainWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute cancel command on the modal
|
// Execute cancel command on any open modal
|
||||||
vm.CreatorConfigEditor.ModalViewModel?.CancelCommand?.Execute(null);
|
vm.CreatorConfigEditor.ModalViewModel?.CancelCommand?.Execute(null);
|
||||||
|
vm.CancelSinglePostOrMessageCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
|
private void OnModalContentClicked(object? sender, PointerPressedEventArgs e) =>
|
||||||
|
|||||||
@ -20,6 +20,19 @@ In GUI mode, the **Help** menu includes:
|
|||||||
- **Documentation** (opens https://docs.ofdl.tools/)
|
- **Documentation** (opens https://docs.ofdl.tools/)
|
||||||
- **About** (shows version details and project/license links)
|
- **About** (shows version details and project/license links)
|
||||||
|
|
||||||
|
In GUI mode, the main download screen includes:
|
||||||
|
|
||||||
|
- **Download Selected** (downloads configured content for selected creators)
|
||||||
|
- **Download Purchased Tab** (downloads purchased tab content)
|
||||||
|
- **Download Single Post/Message** (opens a modal to download one post or one paid message by URL)
|
||||||
|
|
||||||
|
For **Download Single Post/Message** in GUI:
|
||||||
|
|
||||||
|
- Post URL: click `...` on a post and choose **Copy link to post**.
|
||||||
|
- Message URL: only unlocked PPV paid messages are supported by URL. From the main timeline, open **Purchased**,
|
||||||
|
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.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user