forked from sim0n00ps/OF-DL
Initial demo GUI
This commit is contained in:
parent
aee920a9f1
commit
ec8bf47de5
22
AGENTS.md
22
AGENTS.md
@ -2,24 +2,28 @@
|
|||||||
|
|
||||||
Note: Keep AGENTS.md updated as project structure, key services, or workflows change.
|
Note: Keep AGENTS.md updated as project structure, key services, or workflows change.
|
||||||
|
|
||||||
This repo is **OF DL** (also known as OF-DL), a C# console app that downloads media from a user's OnlyFans account(s).
|
This repo is **OF DL** (also known as OF-DL), a C# app suite (console + Avalonia desktop GUI) that downloads media
|
||||||
|
from a user's OnlyFans account(s).
|
||||||
This document is for AI agents helping developers modify the project. It focuses on architecture, data flow, and the
|
This document is for AI agents helping developers modify the project. It focuses on architecture, data flow, and the
|
||||||
most important change points.
|
most important change points.
|
||||||
|
|
||||||
## Quick Flow
|
## Quick Flow
|
||||||
|
|
||||||
1. `Program.Main` builds DI, loads `config.conf`, and runs the interactive flow.
|
1. `Program.Main` is the app entrypoint: default launch is GUI; `--cli` switches to CLI mode.
|
||||||
2. `StartupService.CheckVersionAsync` checks the latest release tag (`OFDLV*`) from `git.ofdl.tools` when not in DEBUG.
|
2. `StartupService.CheckVersionAsync` checks the latest release tag (`OFDLV*`) from `git.ofdl.tools` when not in DEBUG.
|
||||||
3. `StartupService.ValidateEnvironmentAsync` validates OS, FFmpeg, `rules.json`, and Widevine device files.
|
3. `StartupService.ValidateEnvironmentAsync` validates OS, FFmpeg, `rules.json`, and Widevine device files.
|
||||||
4. `AuthService` loads `auth.json` or opens a browser login (PuppeteerSharp) and persists auth data.
|
4. `AuthService` loads `auth.json` or opens a browser login (PuppeteerSharp) and persists auth data.
|
||||||
5. `ApiService` signs every API request with dynamic rules and the current auth.
|
5. `ApiService` signs every API request with dynamic rules and the current auth.
|
||||||
6. `DownloadOrchestrationService` selects creators, prepares folders/DBs, and calls `DownloadService` per media type.
|
6. `DownloadOrchestrationService` selects creators, prepares folders/DBs, and calls `DownloadService` per media type.
|
||||||
7. `DownloadService` downloads media, handles DRM, and records metadata in SQLite.
|
7. `DownloadService` downloads media, handles DRM, and records metadata in SQLite.
|
||||||
|
8. `OF DL.Gui` starts with config validation, then auth validation/browser login, then loads users/lists and provides
|
||||||
|
multi-select download controls.
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
- `OF DL/Program.cs` orchestrates startup, config/auth loading, and the interactive flow (CLI entrypoint).
|
- `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/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.Core/Services/` contains application services (API, auth, download, config, DB, startup, logging, filenames).
|
- `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,
|
- `OF DL.Core/Models/` holds configuration, auth, API request/response models, downloads/startup results, DTOs,
|
||||||
entities, and mapping helpers.
|
entities, and mapping helpers.
|
||||||
@ -86,19 +90,25 @@ most important change points.
|
|||||||
|
|
||||||
## Execution and Testing
|
## Execution and Testing
|
||||||
|
|
||||||
- .NET SDK: 8.x (`net8.0` for all projects).
|
- .NET SDK: 10.x (`net10.0` for all projects).
|
||||||
- Build from the repo root:
|
- Build from the repo root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet build OF DL.sln
|
dotnet build OF DL.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
- Run from source (runtime files are read from the current working directory):
|
- Run from source (GUI mode, default):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project "OF DL/OF DL.csproj"
|
dotnet run --project "OF DL/OF DL.csproj"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Run CLI mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project "OF DL/OF DL.csproj" -- --cli
|
||||||
|
```
|
||||||
|
|
||||||
- If you want a local `rules.json` fallback, run from `OF DL/` or copy `OF DL/rules.json` into your working directory.
|
- If you want a local `rules.json` fallback, run from `OF DL/` or copy `OF DL/rules.json` into your working directory.
|
||||||
- Run tests:
|
- Run tests:
|
||||||
|
|
||||||
@ -220,6 +230,8 @@ cookies/user-agent. Output is written to `{filename}_source.mp4`, then moved and
|
|||||||
## Where to Look First
|
## Where to Look First
|
||||||
|
|
||||||
- `OF DL/Program.cs` for the execution path and menu flow.
|
- `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.Core/Services/ApiService.cs` for OF API calls and header signing.
|
- `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/DownloadService.cs` for downloads and DRM handling.
|
||||||
- `OF DL.Core/Services/DownloadOrchestrationService.cs` for creator selection and flow control.
|
- `OF DL.Core/Services/DownloadOrchestrationService.cs` for creator selection and flow control.
|
||||||
|
|||||||
@ -294,6 +294,8 @@ public class AuthService(IServiceProvider serviceProvider) : IAuthService
|
|||||||
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
|
string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
|
||||||
.Select(key => $"${key}={mappedCookies[key]};"));
|
.Select(key => $"${key}={mappedCookies[key]};"));
|
||||||
|
|
||||||
|
await browser.CloseAsync();
|
||||||
|
|
||||||
return new Auth { Cookie = cookies, UserAgent = userAgent, UserId = userId, XBc = xBc };
|
return new Auth { Cookie = cookies, UserAgent = userAgent, UserId = userId, XBc = xBc };
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|||||||
9
OF DL.Gui/App.axaml
Normal file
9
OF DL.Gui/App.axaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:fluent="clr-namespace:Avalonia.Themes.Fluent;assembly=Avalonia.Themes.Fluent"
|
||||||
|
RequestedThemeVariant="Light"
|
||||||
|
x:Class="OF_DL.Gui.App">
|
||||||
|
<Application.Styles>
|
||||||
|
<fluent:FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
28
OF DL.Gui/App.axaml.cs
Normal file
28
OF DL.Gui/App.axaml.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OF_DL.Gui.Services;
|
||||||
|
using OF_DL.Gui.ViewModels;
|
||||||
|
using OF_DL.Gui.Views;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui;
|
||||||
|
|
||||||
|
public class App : Application
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _serviceProvider = ServiceCollectionFactory.Create().BuildServiceProvider();
|
||||||
|
|
||||||
|
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
MainWindow mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
|
mainWindow.DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>();
|
||||||
|
desktop.MainWindow = mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
OF DL.Gui/OF DL.Gui.csproj
Normal file
28
OF DL.Gui/OF DL.Gui.csproj
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RootNamespace>OF_DL.Gui</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\OF DL.Core\OF DL.Core.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.0.10"/>
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.0.10"/>
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10"/>
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10"/>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
16
OF DL.Gui/Program.cs
Normal file
16
OF DL.Gui/Program.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui;
|
||||||
|
|
||||||
|
public static class GuiLauncher
|
||||||
|
{
|
||||||
|
public static void Run(string[] args)
|
||||||
|
{
|
||||||
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppBuilder BuildAvaloniaApp() =>
|
||||||
|
AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.LogToTrace();
|
||||||
|
}
|
||||||
103
OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs
Normal file
103
OF DL.Gui/Services/AvaloniaDownloadEventHandler.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using OF_DL.Models.Downloads;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Services;
|
||||||
|
|
||||||
|
internal sealed class AvaloniaDownloadEventHandler(
|
||||||
|
Action<string> activitySink,
|
||||||
|
Action<string> progressStatusUpdate,
|
||||||
|
Action<string, long, bool> progressStart,
|
||||||
|
Action<long> progressIncrement,
|
||||||
|
Action progressStop,
|
||||||
|
Func<bool> isCancellationRequested) : IDownloadEventHandler
|
||||||
|
{
|
||||||
|
public async Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStart(statusMessage, 0, false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AvaloniaStatusReporter statusReporter = new(progressStatusUpdate, isCancellationRequested);
|
||||||
|
return await work(statusReporter);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progressStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
|
||||||
|
Func<IProgressReporter, Task<T>> work)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStart(description, maxValue, showSize);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AvaloniaProgressReporter reporter = new(progressIncrement, isCancellationRequested);
|
||||||
|
return await work(reporter);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
progressStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnContentFound(string contentType, int mediaCount, int objectCount)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStatusUpdate($"Found {mediaCount} media from {objectCount} {contentType}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnNoContentFound(string contentType)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStatusUpdate($"Found 0 {contentType}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDownloadComplete(string contentType, DownloadResult result)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStatusUpdate(
|
||||||
|
$"{contentType} complete. Existing: {result.ExistingDownloads}, New: {result.NewDownloads}, Total: {result.TotalCount}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnUserStarting(string username)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
activitySink($"Starting scrape for {username}.");
|
||||||
|
progressStatusUpdate($"Scraping data for {username}...");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnUserComplete(string username, CreatorDownloadResult result)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
activitySink(
|
||||||
|
$"Completed {username}. PaidPosts={result.PaidPostCount}, Posts={result.PostCount}, Archived={result.ArchivedCount}, Streams={result.StreamsCount}, Stories={result.StoriesCount}, Highlights={result.HighlightsCount}, Messages={result.MessagesCount}, PaidMessages={result.PaidMessagesCount}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
activitySink($"Purchased tab complete for {username}. PaidPosts={paidPostCount}, PaidMessages={paidMessagesCount}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnScrapeComplete(TimeSpan elapsed)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
activitySink($"Scrape completed in {elapsed.TotalMinutes:0.00} minutes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnMessage(string message)
|
||||||
|
{
|
||||||
|
ThrowIfCancellationRequested();
|
||||||
|
progressStatusUpdate(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfCancellationRequested()
|
||||||
|
{
|
||||||
|
if (isCancellationRequested())
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation canceled by user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
OF DL.Gui/Services/AvaloniaProgressReporter.cs
Normal file
21
OF DL.Gui/Services/AvaloniaProgressReporter.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Services;
|
||||||
|
|
||||||
|
internal sealed class AvaloniaProgressReporter(
|
||||||
|
Action<long> reportAction,
|
||||||
|
Func<bool> isCancellationRequested) : IProgressReporter
|
||||||
|
{
|
||||||
|
public void ReportProgress(long increment)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested())
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation canceled by user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (increment > 0)
|
||||||
|
{
|
||||||
|
reportAction(increment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
OF DL.Gui/Services/AvaloniaStatusReporter.cs
Normal file
18
OF DL.Gui/Services/AvaloniaStatusReporter.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Services;
|
||||||
|
|
||||||
|
internal sealed class AvaloniaStatusReporter(
|
||||||
|
Action<string> statusAction,
|
||||||
|
Func<bool> isCancellationRequested) : IStatusReporter
|
||||||
|
{
|
||||||
|
public void ReportStatus(string message)
|
||||||
|
{
|
||||||
|
if (isCancellationRequested())
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation canceled by user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
statusAction(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
OF DL.Gui/Services/ConfigValidationService.cs
Normal file
84
OF DL.Gui/Services/ConfigValidationService.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using OF_DL.Models.Config;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Services;
|
||||||
|
|
||||||
|
internal static class ConfigValidationService
|
||||||
|
{
|
||||||
|
public static IReadOnlyDictionary<string, string> Validate(Config config)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> errors = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
ValidatePath(config.DownloadPath, nameof(Config.DownloadPath), errors, requireExistingFile: false);
|
||||||
|
ValidatePath(config.FFmpegPath, nameof(Config.FFmpegPath), errors, requireExistingFile: true);
|
||||||
|
|
||||||
|
if (config.Timeout.HasValue && config.Timeout.Value <= 0 && config.Timeout.Value != -1)
|
||||||
|
{
|
||||||
|
errors[nameof(Config.Timeout)] = "Timeout must be -1 or greater than 0.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.LimitDownloadRate && config.DownloadLimitInMbPerSec <= 0)
|
||||||
|
{
|
||||||
|
errors[nameof(Config.DownloadLimitInMbPerSec)] =
|
||||||
|
"DownloadLimitInMbPerSec must be greater than 0 when LimitDownloadRate is enabled.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.DownloadOnlySpecificDates && !config.CustomDate.HasValue)
|
||||||
|
{
|
||||||
|
errors[nameof(Config.CustomDate)] = "CustomDate is required when DownloadOnlySpecificDates is enabled.";
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateFileNameFormat(config.PaidPostFileNameFormat, nameof(Config.PaidPostFileNameFormat), errors);
|
||||||
|
ValidateFileNameFormat(config.PostFileNameFormat, nameof(Config.PostFileNameFormat), errors);
|
||||||
|
ValidateFileNameFormat(config.PaidMessageFileNameFormat, nameof(Config.PaidMessageFileNameFormat), errors);
|
||||||
|
ValidateFileNameFormat(config.MessageFileNameFormat, nameof(Config.MessageFileNameFormat), errors);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<string, CreatorConfig> creatorConfig in config.CreatorConfigs)
|
||||||
|
{
|
||||||
|
ValidateFileNameFormat(creatorConfig.Value.PaidPostFileNameFormat,
|
||||||
|
$"{nameof(Config.CreatorConfigs)}.{creatorConfig.Key}.PaidPostFileNameFormat", errors);
|
||||||
|
ValidateFileNameFormat(creatorConfig.Value.PostFileNameFormat,
|
||||||
|
$"{nameof(Config.CreatorConfigs)}.{creatorConfig.Key}.PostFileNameFormat", errors);
|
||||||
|
ValidateFileNameFormat(creatorConfig.Value.PaidMessageFileNameFormat,
|
||||||
|
$"{nameof(Config.CreatorConfigs)}.{creatorConfig.Key}.PaidMessageFileNameFormat", errors);
|
||||||
|
ValidateFileNameFormat(creatorConfig.Value.MessageFileNameFormat,
|
||||||
|
$"{nameof(Config.CreatorConfigs)}.{creatorConfig.Key}.MessageFileNameFormat", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePath(string? path, string fieldName, IDictionary<string, string> errors,
|
||||||
|
bool requireExistingFile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.Any(character => Path.GetInvalidPathChars().Contains(character)))
|
||||||
|
{
|
||||||
|
errors[fieldName] = "Path contains invalid characters.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireExistingFile && !File.Exists(path))
|
||||||
|
{
|
||||||
|
errors[fieldName] = "Path must point to an existing file.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateFileNameFormat(string? format, string fieldName, IDictionary<string, string> errors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(format))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasUniqueToken = format.Contains("{mediaId}", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
format.Contains("{filename}", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!hasUniqueToken)
|
||||||
|
{
|
||||||
|
errors[fieldName] = "Format must include {mediaId} or {filename} to avoid file collisions.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
OF DL.Gui/Services/ServiceCollectionFactory.cs
Normal file
29
OF DL.Gui/Services/ServiceCollectionFactory.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OF_DL.Gui.ViewModels;
|
||||||
|
using OF_DL.Gui.Views;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Services;
|
||||||
|
|
||||||
|
internal static class ServiceCollectionFactory
|
||||||
|
{
|
||||||
|
public static IServiceCollection Create()
|
||||||
|
{
|
||||||
|
IServiceCollection services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddSingleton<ILoggingService, LoggingService>();
|
||||||
|
services.AddSingleton<IConfigService, ConfigService>();
|
||||||
|
services.AddSingleton<IAuthService, AuthService>();
|
||||||
|
services.AddSingleton<IApiService, ApiService>();
|
||||||
|
services.AddSingleton<IDbService, DbService>();
|
||||||
|
services.AddSingleton<IDownloadService, DownloadService>();
|
||||||
|
services.AddSingleton<IFileNameService, FileNameService>();
|
||||||
|
services.AddSingleton<IStartupService, StartupService>();
|
||||||
|
services.AddSingleton<IDownloadOrchestrationService, DownloadOrchestrationService>();
|
||||||
|
|
||||||
|
services.AddSingleton<MainWindowViewModel>();
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
OF DL.Gui/ViewModels/AppScreen.cs
Normal file
10
OF DL.Gui/ViewModels/AppScreen.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public enum AppScreen
|
||||||
|
{
|
||||||
|
Loading,
|
||||||
|
Config,
|
||||||
|
Auth,
|
||||||
|
UserSelection,
|
||||||
|
Error
|
||||||
|
}
|
||||||
22
OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs
Normal file
22
OF DL.Gui/ViewModels/ConfigCategoryViewModel.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public sealed class ConfigCategoryViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public ConfigCategoryViewModel(string categoryName, IEnumerable<ConfigFieldViewModel> fields)
|
||||||
|
{
|
||||||
|
CategoryName = categoryName;
|
||||||
|
foreach (ConfigFieldViewModel field in fields)
|
||||||
|
{
|
||||||
|
Fields.Add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CategoryName { get; }
|
||||||
|
|
||||||
|
public bool IsDownloadBehavior =>
|
||||||
|
string.Equals(CategoryName, "Download Behavior", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public ObservableCollection<ConfigFieldViewModel> Fields { get; } = [];
|
||||||
|
}
|
||||||
251
OF DL.Gui/ViewModels/ConfigFieldViewModel.cs
Normal file
251
OF DL.Gui/ViewModels/ConfigFieldViewModel.cs
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Reflection;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public partial class ConfigFieldViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public ConfigFieldViewModel(PropertyInfo propertyInfo, object? initialValue)
|
||||||
|
{
|
||||||
|
PropertyInfo = propertyInfo;
|
||||||
|
PropertyName = propertyInfo.Name;
|
||||||
|
DisplayName = ToDisplayName(propertyInfo.Name);
|
||||||
|
PropertyType = propertyInfo.PropertyType;
|
||||||
|
|
||||||
|
IsBoolean = PropertyType == typeof(bool);
|
||||||
|
IsEnum = PropertyType.IsEnum;
|
||||||
|
IsDate = PropertyType == typeof(DateTime?);
|
||||||
|
IsNumeric = PropertyType == typeof(int) || PropertyType == typeof(int?);
|
||||||
|
IsMultiline = PropertyType == typeof(Dictionary<string, CreatorConfig>);
|
||||||
|
IsTextInput = !IsBoolean && !IsEnum && !IsDate && !IsNumeric;
|
||||||
|
|
||||||
|
if (IsEnum)
|
||||||
|
{
|
||||||
|
foreach (string enumName in Enum.GetNames(PropertyType))
|
||||||
|
{
|
||||||
|
EnumOptions.Add(enumName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadInitialValue(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PropertyInfo PropertyInfo { get; }
|
||||||
|
|
||||||
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public Type PropertyType { get; }
|
||||||
|
|
||||||
|
public bool IsBoolean { get; }
|
||||||
|
|
||||||
|
public bool IsEnum { get; }
|
||||||
|
|
||||||
|
public bool IsDate { get; }
|
||||||
|
|
||||||
|
public bool IsNumeric { get; }
|
||||||
|
|
||||||
|
public bool IsTextInput { get; }
|
||||||
|
|
||||||
|
public bool IsMultiline { get; }
|
||||||
|
|
||||||
|
public double TextBoxMinHeight => IsMultiline ? 150 : 36;
|
||||||
|
|
||||||
|
public ObservableCollection<string> EnumOptions { get; } = [];
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _boolValue;
|
||||||
|
|
||||||
|
[ObservableProperty] private string? _enumValue;
|
||||||
|
|
||||||
|
[ObservableProperty] private DateTimeOffset? _dateValue;
|
||||||
|
|
||||||
|
[ObservableProperty] private decimal? _numericValue;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _textValue = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasError))]
|
||||||
|
private string _errorMessage = string.Empty;
|
||||||
|
|
||||||
|
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||||
|
|
||||||
|
public bool TryGetTypedValue(out object? value, out string? error)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
error = null;
|
||||||
|
|
||||||
|
if (IsBoolean)
|
||||||
|
{
|
||||||
|
value = BoolValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEnum)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(EnumValue))
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} is required.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Enum.TryParse(PropertyType, EnumValue, true, out object? enumResult))
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} must be one of: {string.Join(", ", EnumOptions)}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = enumResult;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
value = TextValue.Trim();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(int))
|
||||||
|
{
|
||||||
|
if (!NumericValue.HasValue)
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} must be a whole number.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimal.Truncate(NumericValue.Value) != NumericValue.Value)
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} must be a whole number.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = (int)NumericValue.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(int?))
|
||||||
|
{
|
||||||
|
if (!NumericValue.HasValue)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimal.Truncate(NumericValue.Value) != NumericValue.Value)
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} must be a whole number.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = (int)NumericValue.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(DateTime?))
|
||||||
|
{
|
||||||
|
if (!DateValue.HasValue)
|
||||||
|
{
|
||||||
|
value = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = DateValue.Value.DateTime;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(Dictionary<string, CreatorConfig>))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(TextValue))
|
||||||
|
{
|
||||||
|
value = new Dictionary<string, CreatorConfig>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, CreatorConfig>? parsed =
|
||||||
|
JsonConvert.DeserializeObject<Dictionary<string, CreatorConfig>>(TextValue);
|
||||||
|
value = parsed ?? new Dictionary<string, CreatorConfig>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
error = $"{DisplayName} must be valid JSON.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error = $"{DisplayName} has an unsupported field type.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearError()
|
||||||
|
{
|
||||||
|
ErrorMessage = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetError(string message)
|
||||||
|
{
|
||||||
|
ErrorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadInitialValue(object? initialValue)
|
||||||
|
{
|
||||||
|
if (IsBoolean)
|
||||||
|
{
|
||||||
|
BoolValue = initialValue is bool boolValue && boolValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEnum)
|
||||||
|
{
|
||||||
|
EnumValue = initialValue?.ToString() ?? EnumOptions.FirstOrDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(Dictionary<string, CreatorConfig>))
|
||||||
|
{
|
||||||
|
Dictionary<string, CreatorConfig> creatorConfigs =
|
||||||
|
initialValue as Dictionary<string, CreatorConfig> ?? new Dictionary<string, CreatorConfig>();
|
||||||
|
TextValue = JsonConvert.SerializeObject(creatorConfigs, Formatting.Indented);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(DateTime?))
|
||||||
|
{
|
||||||
|
DateTime? date = initialValue is DateTime dt ? dt : null;
|
||||||
|
DateValue = date.HasValue ? new DateTimeOffset(date.Value) : null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(int))
|
||||||
|
{
|
||||||
|
NumericValue = initialValue is int intValue ? intValue : 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PropertyType == typeof(int?))
|
||||||
|
{
|
||||||
|
NumericValue = initialValue is int nullableIntValue ? nullableIntValue : null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextValue = initialValue?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToDisplayName(string propertyName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(propertyName))
|
||||||
|
{
|
||||||
|
return propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Concat(propertyName.Select((character, index) =>
|
||||||
|
index > 0 && char.IsUpper(character) && !char.IsUpper(propertyName[index - 1])
|
||||||
|
? $" {character}"
|
||||||
|
: character.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
1198
OF DL.Gui/ViewModels/MainWindowViewModel.cs
Normal file
1198
OF DL.Gui/ViewModels/MainWindowViewModel.cs
Normal file
File diff suppressed because it is too large
Load Diff
19
OF DL.Gui/ViewModels/MultiSelectOptionViewModel.cs
Normal file
19
OF DL.Gui/ViewModels/MultiSelectOptionViewModel.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public partial class MultiSelectOptionViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public MultiSelectOptionViewModel(string displayName, string propertyName, bool isSelected)
|
||||||
|
{
|
||||||
|
DisplayName = displayName;
|
||||||
|
PropertyName = propertyName;
|
||||||
|
IsSelected = isSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
}
|
||||||
12
OF DL.Gui/ViewModels/SelectableUserViewModel.cs
Normal file
12
OF DL.Gui/ViewModels/SelectableUserViewModel.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public partial class SelectableUserViewModel(string username, long userId) : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Username { get; } = username;
|
||||||
|
|
||||||
|
public long UserId { get; } = userId;
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
}
|
||||||
7
OF DL.Gui/ViewModels/ViewModelBase.cs
Normal file
7
OF DL.Gui/ViewModels/ViewModelBase.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
public abstract class ViewModelBase : ObservableObject
|
||||||
|
{
|
||||||
|
}
|
||||||
394
OF DL.Gui/Views/MainWindow.axaml
Normal file
394
OF DL.Gui/Views/MainWindow.axaml
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
<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:vm="using:OF_DL.Gui.ViewModels"
|
||||||
|
x:Class="OF_DL.Gui.Views.MainWindow"
|
||||||
|
x:DataType="vm:MainWindowViewModel"
|
||||||
|
Width="1320"
|
||||||
|
Height="860"
|
||||||
|
MinWidth="900"
|
||||||
|
MinHeight="700"
|
||||||
|
Title="OF DL"
|
||||||
|
Background="#EEF3FB"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Border.surface">
|
||||||
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
<Setter Property="BorderBrush" Value="#DDE5F3" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.primary">
|
||||||
|
<Setter Property="Background" Value="#2E6EEA" />
|
||||||
|
<Setter Property="Foreground" Value="#FFFFFF" />
|
||||||
|
<Setter Property="Padding" Value="14,8" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.secondary">
|
||||||
|
<Setter Property="Background" Value="#FFFFFF" />
|
||||||
|
<Setter Property="Foreground" Value="#1F2A44" />
|
||||||
|
<Setter Property="Padding" Value="14,8" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="BorderBrush" Value="#CFD9EB" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*">
|
||||||
|
<Menu Grid.Row="0">
|
||||||
|
<MenuItem Header="_File">
|
||||||
|
<MenuItem Header="_Logout"
|
||||||
|
IsVisible="{Binding IsAuthenticated}"
|
||||||
|
Command="{Binding LogoutCommand}" />
|
||||||
|
<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>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Padding="16"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
BorderBrush="#CFD9EB"
|
||||||
|
Background="#DDEAFF">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Foreground="#304261"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="{Binding AuthenticatedUserDisplay}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button IsVisible="{Binding IsDownloading}"
|
||||||
|
Classes="primary"
|
||||||
|
Background="#D84E4E"
|
||||||
|
Command="{Binding StopWorkCommand}"
|
||||||
|
Content="Stop" />
|
||||||
|
<Button Classes="secondary"
|
||||||
|
IsVisible="{Binding IsAuthenticated}"
|
||||||
|
Command="{Binding LogoutCommand}"
|
||||||
|
Content="Logout" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Margin="14">
|
||||||
|
<StackPanel IsVisible="{Binding IsLoadingScreen}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="12"
|
||||||
|
Width="420">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
FontSize="22"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="Loading" />
|
||||||
|
<ProgressBar IsIndeterminate="True" Height="14" />
|
||||||
|
<TextBlock HorizontalAlignment="Center" Text="{Binding LoadingMessage}" TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer IsVisible="{Binding IsConfigScreen}">
|
||||||
|
<StackPanel Spacing="14" Margin="2,0,4,0">
|
||||||
|
<TextBlock FontSize="22" FontWeight="SemiBold" Text="Configuration" />
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="3*,5*" Margin="0,0,0,2">
|
||||||
|
<Border Grid.Column="0" Classes="surface" Padding="12" Margin="0,0,10,0">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#1F2A44" Text="External" />
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="FFmpeg Path" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding FfmpegPath}"
|
||||||
|
Watermark="Select ffmpeg executable" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Classes="secondary"
|
||||||
|
Content="Browse..."
|
||||||
|
Click="OnBrowseFfmpegPathClick" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock IsVisible="{Binding HasFfmpegPathError}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding FfmpegPathError}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Classes="surface" Padding="12">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock FontSize="16"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="Download Media Types" />
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Media Types" />
|
||||||
|
<ItemsControl ItemsSource="{Binding MediaTypeOptions}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||||
|
<CheckBox Margin="0,0,14,6"
|
||||||
|
Content="{Binding DisplayName}"
|
||||||
|
IsChecked="{Binding IsSelected}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock IsVisible="{Binding HasMediaTypesError}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding MediaTypesError}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Sources" />
|
||||||
|
<ItemsControl ItemsSource="{Binding MediaSourceOptions}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:MultiSelectOptionViewModel">
|
||||||
|
<CheckBox Margin="0,0,14,6"
|
||||||
|
Content="{Binding DisplayName}"
|
||||||
|
IsChecked="{Binding IsSelected}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock IsVisible="{Binding HasMediaSourcesError}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding MediaSourcesError}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Classes="surface" Padding="12">
|
||||||
|
<ItemsControl ItemsSource="{Binding ConfigCategories}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ConfigCategoryViewModel">
|
||||||
|
<Border Classes="surface"
|
||||||
|
Width="600"
|
||||||
|
Margin="0,0,12,12"
|
||||||
|
Padding="10">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock FontSize="16"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="{Binding CategoryName}" />
|
||||||
|
<StackPanel IsVisible="{Binding IsDownloadBehavior}"
|
||||||
|
Spacing="4"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
x:CompileBindings="False">
|
||||||
|
<TextBlock FontWeight="SemiBold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="Download Path" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Text="{Binding DataContext.DownloadPath, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
Watermark="Select download folder" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Classes="secondary"
|
||||||
|
Content="Browse..."
|
||||||
|
Click="OnBrowseDownloadPathClick" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock IsVisible="{Binding DataContext.HasDownloadPathError, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding DataContext.DownloadPathError, RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
<ItemsControl ItemsSource="{Binding Fields}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ConfigFieldViewModel">
|
||||||
|
<Grid Margin="0,0,0,10" ColumnDefinitions="190,*">
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Margin="0,6,10,0"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="#1F2A44"
|
||||||
|
Text="{Binding DisplayName}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<StackPanel Grid.Column="1" Spacing="4">
|
||||||
|
<CheckBox IsVisible="{Binding IsBoolean}"
|
||||||
|
IsChecked="{Binding BoolValue}" />
|
||||||
|
|
||||||
|
<ComboBox IsVisible="{Binding IsEnum}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{Binding EnumOptions}"
|
||||||
|
SelectedItem="{Binding EnumValue}" />
|
||||||
|
|
||||||
|
<DatePicker IsVisible="{Binding IsDate}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
SelectedDate="{Binding DateValue}" />
|
||||||
|
|
||||||
|
<NumericUpDown IsVisible="{Binding IsNumeric}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Value="{Binding NumericValue}"
|
||||||
|
Increment="1"
|
||||||
|
FormatString="N0" />
|
||||||
|
|
||||||
|
<TextBox IsVisible="{Binding IsTextInput}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
AcceptsReturn="{Binding IsMultiline}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MinHeight="{Binding TextBoxMinHeight}"
|
||||||
|
Text="{Binding TextValue}" />
|
||||||
|
|
||||||
|
<TextBlock IsVisible="{Binding HasError}"
|
||||||
|
Foreground="#FF5A5A"
|
||||||
|
Text="{Binding ErrorMessage}"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Save Configuration"
|
||||||
|
Classes="primary"
|
||||||
|
Command="{Binding SaveConfigCommand}" />
|
||||||
|
<Button Content="Cancel"
|
||||||
|
Classes="secondary"
|
||||||
|
Command="{Binding CancelConfigCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<StackPanel IsVisible="{Binding IsAuthScreen}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Width="640"
|
||||||
|
Spacing="14">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="Authentication Required" />
|
||||||
|
<TextBlock TextWrapping="Wrap"
|
||||||
|
TextAlignment="Center"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="{Binding AuthScreenMessage}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
|
||||||
|
<Button Content="Login with Browser"
|
||||||
|
Classes="primary"
|
||||||
|
Command="{Binding StartBrowserLoginCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid IsVisible="{Binding IsUserSelectionScreen}" RowDefinitions="Auto,*,Auto" ColumnDefinitions="2*,3*">
|
||||||
|
<Border Grid.Row="0"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Padding="10"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
Classes="surface">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<Button Content="Select All" Classes="secondary" Command="{Binding SelectAllUsersCommand}" />
|
||||||
|
<Button Content="Select None" Classes="secondary" Command="{Binding SelectNoUsersCommand}" />
|
||||||
|
<ComboBox Width="280"
|
||||||
|
PlaceholderText="Select a list of users"
|
||||||
|
ItemsSource="{Binding UserLists}"
|
||||||
|
SelectedItem="{Binding SelectedListName}" />
|
||||||
|
<Button Content="Download Purchased Tab"
|
||||||
|
Classes="secondary"
|
||||||
|
Command="{Binding DownloadPurchasedTabCommand}" />
|
||||||
|
<Button Content="Download Selected"
|
||||||
|
Classes="primary"
|
||||||
|
Command="{Binding DownloadSelectedCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
Padding="10"
|
||||||
|
Classes="surface">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="3">
|
||||||
|
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44" Text="Users" />
|
||||||
|
<TextBlock Foreground="#4A5B78" Text="{Binding SelectedUsersSummary}" />
|
||||||
|
</StackPanel>
|
||||||
|
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding AvailableUsers}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SelectableUserViewModel">
|
||||||
|
<CheckBox Content="{Binding Username}" IsChecked="{Binding IsSelected}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
Padding="10"
|
||||||
|
Classes="surface">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<TextBlock Grid.Row="0" FontWeight="SemiBold" Foreground="#1F2A44" Text="Activity Log" />
|
||||||
|
<ListBox Grid.Row="1" Margin="0,8,0,0" ItemsSource="{Binding ActivityLog}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Margin="0,10,0,0"
|
||||||
|
Padding="10"
|
||||||
|
Classes="surface"
|
||||||
|
IsVisible="{Binding IsDownloadProgressVisible}">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock FontWeight="SemiBold" Foreground="#1F2A44"
|
||||||
|
Text="{Binding DownloadProgressDescription}" />
|
||||||
|
<ProgressBar IsIndeterminate="{Binding IsDownloadProgressIndeterminate}"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="{Binding DownloadProgressMaximum}"
|
||||||
|
Value="{Binding DownloadProgressValue}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel IsVisible="{Binding IsErrorScreen}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Width="640"
|
||||||
|
Spacing="14">
|
||||||
|
<TextBlock HorizontalAlignment="Center"
|
||||||
|
FontSize="24"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="Startup Error" />
|
||||||
|
<TextBlock TextWrapping="Wrap" Text="{Binding ErrorMessage}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
|
||||||
|
<Button Content="Retry Startup" Classes="primary" Command="{Binding RetryStartupCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
103
OF DL.Gui/Views/MainWindow.axaml.cs
Normal file
103
OF DL.Gui/Views/MainWindow.axaml.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using OF_DL.Gui.ViewModels;
|
||||||
|
|
||||||
|
namespace OF_DL.Gui.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private bool _hasInitialized;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Opened += OnOpened;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnOpened(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_hasInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasInitialized = true;
|
||||||
|
if (DataContext is MainWindowViewModel vm)
|
||||||
|
{
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnBrowseFfmpegPathClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not MainWindowViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TopLevel? topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel?.StorageProvider == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<IStorageFile> selectedFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
|
||||||
|
new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = "Select FFmpeg executable",
|
||||||
|
AllowMultiple = false
|
||||||
|
});
|
||||||
|
|
||||||
|
IStorageFile? selectedFile = selectedFiles.FirstOrDefault();
|
||||||
|
if (selectedFile == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? localPath = selectedFile.TryGetLocalPath();
|
||||||
|
if (!string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
vm.SetFfmpegPath(localPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.SetFfmpegPath(selectedFile.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnBrowseDownloadPathClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not MainWindowViewModel vm)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TopLevel? topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
if (topLevel?.StorageProvider == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<IStorageFolder> selectedFolders = await topLevel.StorageProvider.OpenFolderPickerAsync(
|
||||||
|
new FolderPickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = "Select download folder",
|
||||||
|
AllowMultiple = false
|
||||||
|
});
|
||||||
|
|
||||||
|
IStorageFolder? selectedFolder = selectedFolders.FirstOrDefault();
|
||||||
|
if (selectedFolder == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? localPath = selectedFolder.TryGetLocalPath();
|
||||||
|
if (!string.IsNullOrWhiteSpace(localPath))
|
||||||
|
{
|
||||||
|
vm.SetDownloadPath(localPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.SetDownloadPath(selectedFolder.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Core", "OF DL.Core\OF
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Tests", "OF DL.Tests\OF DL.Tests.csproj", "{FF5EC4D7-6369-4A78-8C02-E370343E797C}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Tests", "OF DL.Tests\OF DL.Tests.csproj", "{FF5EC4D7-6369-4A78-8C02-E370343E797C}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OF DL.Gui", "OF DL.Gui\OF DL.Gui.csproj", "{495749B1-DD15-4637-85AA-49841A86A510}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -27,6 +29,10 @@ Global
|
|||||||
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{FF5EC4D7-6369-4A78-8C02-E370343E797C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{495749B1-DD15-4637-85AA-49841A86A510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{495749B1-DD15-4637-85AA-49841A86A510}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{495749B1-DD15-4637-85AA-49841A86A510}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{495749B1-DD15-4637-85AA-49841A86A510}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OF DL.Core\OF DL.Core.csproj"/>
|
<ProjectReference Include="..\OF DL.Core\OF DL.Core.csproj"/>
|
||||||
|
<ProjectReference Include="..\OF DL.Gui\OF DL.Gui.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OF_DL.CLI;
|
using OF_DL.CLI;
|
||||||
|
using OF_DL.Gui;
|
||||||
using OF_DL.Models;
|
using OF_DL.Models;
|
||||||
using OF_DL.Enumerations;
|
using OF_DL.Enumerations;
|
||||||
using OF_DL.Models.Config;
|
using OF_DL.Models.Config;
|
||||||
@ -14,6 +15,9 @@ namespace OF_DL;
|
|||||||
|
|
||||||
public class Program(IServiceProvider serviceProvider)
|
public class Program(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
|
private const string CliFlag = "--cli";
|
||||||
|
private const string NonInteractiveFlag = "--non-interactive";
|
||||||
|
|
||||||
private async Task LoadAuthFromBrowser()
|
private async Task LoadAuthFromBrowser()
|
||||||
{
|
{
|
||||||
IAuthService authService = serviceProvider.GetRequiredService<IAuthService>();
|
IAuthService authService = serviceProvider.GetRequiredService<IAuthService>();
|
||||||
@ -65,13 +69,23 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
await authService.SaveToFileAsync();
|
await authService.SaveToFileAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[STAThread]
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
|
bool runCli = await ShouldRunCliModeAsync(args);
|
||||||
|
string[] cleanArgs = args.Where(a => !a.Equals(CliFlag, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||||
|
|
||||||
|
if (!runCli)
|
||||||
|
{
|
||||||
|
GuiLauncher.Run(cleanArgs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
|
AnsiConsole.Write(new FigletText("Welcome to OF-DL").Color(Color.Red));
|
||||||
AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n");
|
AnsiConsole.Markup("Documentation: [link]https://docs.ofdl.tools/[/]\n");
|
||||||
AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n");
|
AnsiConsole.Markup("Discord server: [link]https://discord.com/invite/6bUW8EJ53j[/]\n\n");
|
||||||
|
|
||||||
ServiceCollection services = await ConfigureServices(args);
|
ServiceCollection services = await ConfigureServices(cleanArgs);
|
||||||
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
// Get the Program instance and run
|
// Get the Program instance and run
|
||||||
@ -79,6 +93,34 @@ public class Program(IServiceProvider serviceProvider)
|
|||||||
await program.RunAsync();
|
await program.RunAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> ShouldRunCliModeAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Any(a => a.Equals(CliFlag, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Any(a => a.Equals(NonInteractiveFlag, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceCollection services = new();
|
||||||
|
services.AddSingleton<ILoggingService, LoggingService>();
|
||||||
|
services.AddSingleton<IConfigService, ConfigService>();
|
||||||
|
|
||||||
|
await using ServiceProvider provider = services.BuildServiceProvider();
|
||||||
|
IConfigService configService = provider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
|
bool loaded = await configService.LoadConfigurationAsync(args);
|
||||||
|
if (!loaded)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configService.IsCliNonInteractive || configService.CurrentConfig.NonInteractiveMode;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<ServiceCollection> ConfigureServices(string[] args)
|
private static async Task<ServiceCollection> ConfigureServices(string[] args)
|
||||||
{
|
{
|
||||||
// Set up dependency injection with LoggingService and ConfigService
|
// Set up dependency injection with LoggingService and ConfigService
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user