remove DeepDrftCli project and CLI.sln; clean orphan GUID from DeepDrftHome.sln
This commit is contained in:
@@ -1,40 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCli", "DeepDrftCli\DeepDrftCli.csproj", "{84844B37-FD15-4AFC-850B-DD432AA33B4C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{A3DA341B-589E-4705-AB66-6B22652A9B36}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetBlocks", "C:\lib\NetBlocks\NetBlocks.csproj", "{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftModels", "DeepDrftModels\DeepDrftModels.csproj", "{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{84844B37-FD15-4AFC-850B-DD432AA33B4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{84844B37-FD15-4AFC-850B-DD432AA33B4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{84844B37-FD15-4AFC-850B-DD432AA33B4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{84844B37-FD15-4AFC-850B-DD432AA33B4C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A3DA341B-589E-4705-AB66-6B22652A9B36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A3DA341B-589E-4705-AB66-6B22652A9B36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A3DA341B-589E-4705-AB66-6B22652A9B36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A3DA341B-589E-4705-AB66-6B22652A9B36}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{AEA0B3A0-722E-4D34-B2F6-F8179A4DD45A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
# CLAUDE.md - DeepDrftCli
|
|
||||||
|
|
||||||
Guidance for working in the DeepDrftCli project (the admin CLI tool).
|
|
||||||
|
|
||||||
See the root `CLAUDE.md` for full architecture overview. This file covers what is specific to this project.
|
|
||||||
|
|
||||||
## One-line purpose
|
|
||||||
|
|
||||||
Local admin tool. Adds and lists tracks. Two modes: classic CLI (`add` / `list` / `help`) and Terminal.Gui interactive interface (`gui`). Has direct access to both the SQL DB and the FileDatabase (it's not a network client — it runs on the same machine as the databases).
|
|
||||||
|
|
||||||
## Why this is allowed to bypass the API
|
|
||||||
|
|
||||||
CLI is a **local single-user admin tool**, run on the same machine as the databases by an admin. It consumes `DeepDrftWeb.Services` and `DeepDrftContent.Services` directly without going through HTTP. Browsers never get to bypass — the APIs are for them. **Don't extend this pattern to network clients.**
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
DeepDrftCli/
|
|
||||||
├── Services/
|
|
||||||
│ ├── CliService.cs # Main CLI command dispatcher
|
|
||||||
│ └── GuiService.cs # Terminal.Gui interface
|
|
||||||
├── Models/
|
|
||||||
│ └── CliSettings.cs # Config POCO (ConnectionString, VaultPath)
|
|
||||||
├── Program.cs # Entry point, DI setup
|
|
||||||
├── environment/
|
|
||||||
│ ├── connections.json # The actual config file (not appsettings.json)
|
|
||||||
│ └── config.json # Placeholder, currently unused
|
|
||||||
└── DeepDrftCli.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`CliSettings` (loaded from `environment/connections.json`):
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class CliSettings
|
|
||||||
{
|
|
||||||
public required string ConnectionString { get; set; } // SQLite path
|
|
||||||
public required string VaultPath { get; set; } // FileDatabase root
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**The config file is `environment/connections.json`, not `appsettings.json`.** Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"CliSettings": {
|
|
||||||
"ConnectionString": "Data Source=../Database/deepdrft.db",
|
|
||||||
"VaultPath": "../Database/Vaults"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Paths are resolved relative to `AppDomain.CurrentDomain.BaseDirectory`.
|
|
||||||
|
|
||||||
## DI wiring (Program.cs)
|
|
||||||
|
|
||||||
Registers:
|
|
||||||
- `DeepDrftContext` with SQLite (from connection string)
|
|
||||||
- `FileDatabase` singleton (awaited on init via `FileDatabase.FromAsync`)
|
|
||||||
- `TrackRepository`, `DeepDrftWeb.Services.TrackService` (SQL side)
|
|
||||||
- `AudioProcessor`, `DeepDrftContent.Services.TrackService` (content side)
|
|
||||||
- `CliService`, `GuiService`
|
|
||||||
- Console logging
|
|
||||||
|
|
||||||
All services are scoped or singletons. The app waits for `FileDatabase.FromAsync` on startup (blocking), so the vault is ready before any command runs.
|
|
||||||
|
|
||||||
## Mode dispatch
|
|
||||||
|
|
||||||
`Program.cs` checks the first argument:
|
|
||||||
- `gui` or `--gui`: runs `GuiService.RunAsync()` → Terminal.Gui interactive interface.
|
|
||||||
- Anything else: runs `CliService.RunAsync(args)` → classic CLI command parsing.
|
|
||||||
|
|
||||||
## CLI commands (classic mode)
|
|
||||||
|
|
||||||
### add <wav-file> <track-name> <artist> [album] [genre] [release-date]
|
|
||||||
|
|
||||||
Positional arguments. Adds a track:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DeepDrftCli add "/path/to/song.wav" "My Song" "Artist Name" "Album Title" "Rock" "2024-01-15"
|
|
||||||
```
|
|
||||||
|
|
||||||
Release date format: `YYYY-MM-DD`. Optional: album, genre, release date.
|
|
||||||
|
|
||||||
### add <wav-file> -i|--interactive [defaults...]
|
|
||||||
|
|
||||||
Interactive mode. Prompts for each field; command-line args become defaults:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DeepDrftCli add "/path/to/song.wav" -i "Artist Name"
|
|
||||||
```
|
|
||||||
|
|
||||||
Prompts: "Track Name? [default] → ", "Artist? [default] → ", etc.
|
|
||||||
|
|
||||||
### list
|
|
||||||
|
|
||||||
Formats and displays all tracks from SQL:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
DeepDrftCli list
|
|
||||||
```
|
|
||||||
|
|
||||||
Table output: ID, Name, Artist, Album, Genre, ReleaseDate, EntryKey, ImagePath.
|
|
||||||
|
|
||||||
### help, --help, -h
|
|
||||||
|
|
||||||
Shows command list.
|
|
||||||
|
|
||||||
### gui (or --gui)
|
|
||||||
|
|
||||||
Launches the Terminal.Gui interactive interface (separate mode).
|
|
||||||
|
|
||||||
## GUI mode (Terminal.Gui)
|
|
||||||
|
|
||||||
Launches a full interactive terminal UI with:
|
|
||||||
- **DeepDrft brand color scheme** (Magenta/Purple/Pink).
|
|
||||||
- **Track list** (scrollable, selectable, deletable).
|
|
||||||
- **Status pane** showing feedback and errors.
|
|
||||||
- **Persistent hotkey legend** (F1=Help, A=Add, D=Delete, Q=Quit, etc.).
|
|
||||||
- **Add dialog** with file browser and track metadata entry.
|
|
||||||
- **Keyboard navigation**: arrow keys to navigate, Enter to select, escape to cancel.
|
|
||||||
|
|
||||||
`GuiService.RunAsync()` creates a `Window`, populates controls, and runs the main loop.
|
|
||||||
|
|
||||||
## Dual-database add flow (critical contract)
|
|
||||||
|
|
||||||
This is the seam where both databases are written — **must be idempotent and crash-safe**:
|
|
||||||
|
|
||||||
1. `DeepDrftContent.Services.TrackService.AddTrackFromWavAsync(filePath)`:
|
|
||||||
- Validates file is `.wav` and readable.
|
|
||||||
- Calls `AudioProcessor.ProcessWavFileAsync(filePath)` → `AudioBinary` (duration, bitrate extracted).
|
|
||||||
- Generates a GUID entry key.
|
|
||||||
- Ensures the `tracks` vault exists.
|
|
||||||
- Calls `FileDatabase.RegisterResourceAsync("tracks", entryKey, audioBinary)`.
|
|
||||||
- Returns a populated `TrackEntity` (with `Id = 0` since not yet in SQL).
|
|
||||||
|
|
||||||
2. `DeepDrftWeb.Services.TrackService.Create(trackEntity)`:
|
|
||||||
- Saves the entity to SQLite.
|
|
||||||
- Returns the persisted entity with `Id` assigned.
|
|
||||||
|
|
||||||
**If step 1 succeeds and step 2 fails, the audio is orphaned in the vault.** There is no compensating rollback today. The CLI logs both results and exits with status code indicating overall success/failure.
|
|
||||||
|
|
||||||
## File validation
|
|
||||||
|
|
||||||
- **Extension**: must be `.wav` (only WAV files are supported).
|
|
||||||
- **Existence**: checked before processing.
|
|
||||||
- **Readability**: attempted during processing.
|
|
||||||
- **WAV structure**: validated by `AudioProcessor` (RIFF/WAVE/PCM header parsing).
|
|
||||||
|
|
||||||
Non-WAV files are rejected with a clear error message.
|
|
||||||
|
|
||||||
## Release date format
|
|
||||||
|
|
||||||
Only `YYYY-MM-DD` is accepted. Parsing is strict:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
DateOnly.ParseExact(releaseDateStr, "yyyy-MM-dd")
|
|
||||||
```
|
|
||||||
|
|
||||||
Invalid dates result in an error. Optional fields (if not provided or invalid) are set to `null`.
|
|
||||||
|
|
||||||
## Publishing
|
|
||||||
|
|
||||||
Build configuration in `.csproj`:
|
|
||||||
- `PublishSingleFile=true`: Produces a single executable.
|
|
||||||
- `SelfContained` and `IncludeNativeLibrariesForSelfExtract` are **commented out** — publish is framework-dependent (requires .NET runtime installed).
|
|
||||||
|
|
||||||
To publish:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish
|
|
||||||
```
|
|
||||||
|
|
||||||
Result is a single `.exe` (Windows) or binary (Linux) that consumes the host framework.
|
|
||||||
|
|
||||||
## Service registration patterns
|
|
||||||
|
|
||||||
All service calls return `Result` or `ResultContainer<T>`. CLI checks `Success` and `Messages` to display feedback:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
var result = await _webTrackService.Create(trackEntity);
|
|
||||||
if (result.Success && result.Value != null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"✓ Saved to SQL with ID {result.Value.Id}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"✗ SQL save failed: {string.Join("; ", result.Messages.Select(m => m.Message))}");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build
|
|
||||||
dotnet build DeepDrftCli
|
|
||||||
|
|
||||||
# Run classic CLI mode
|
|
||||||
dotnet run --project DeepDrftCli -- add "test.wav" "Test Track" "Test Artist"
|
|
||||||
dotnet run --project DeepDrftCli -- list
|
|
||||||
dotnet run --project DeepDrftCli -- help
|
|
||||||
|
|
||||||
# Run GUI mode
|
|
||||||
dotnet run --project DeepDrftCli -- gui
|
|
||||||
|
|
||||||
# Build for single-file publish
|
|
||||||
dotnet publish DeepDrftCli -c Release -o ./bin/Release/publish
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important patterns
|
|
||||||
|
|
||||||
- **No HTTP**: CLI talks directly to databases. No HTTP clients needed.
|
|
||||||
- **Async/await**: All database operations are async. Even CLI mode runs in an async context.
|
|
||||||
- **Error swallowing in FileDatabase**: Vault operations return `null` / `false`. CLI must check return values.
|
|
||||||
- **Local-only**: CLI has no authentication or CORS. It runs locally as an admin tool.
|
|
||||||
- **Rollback**: Adding a track is not transactional across the two databases. If one fails, the other may have partially succeeded (orphaned audio in the vault). Plan accordingly.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- `environment/connections.json`: **required** at runtime. Must contain `CliSettings` with `ConnectionString` and `VaultPath`.
|
|
||||||
- `environment/config.json`: Placeholder, currently unused. Keep it as a placeholder for future expansion.
|
|
||||||
- No `appsettings.json` — don't add one.
|
|
||||||
|
|
||||||
When working with this project, focus on the dual-database consistency of the add flow, CLI argument parsing, and maintaining the interactive Terminal.Gui mode as a first-class interface (not a second-class citizen).
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
|
||||||
<!-- Npgsql 10.0.1 requires Microsoft.EntityFrameworkCore >= 10.0.4; keep in sync -->
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.7" />
|
|
||||||
<PackageReference Include="Terminal.Gui" Version="1.19.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
|
|
||||||
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
|
|
||||||
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="environment\config.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="environment\connections.json">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Enable single file publish -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<!-- <SelfContained>true</SelfContained>-->
|
|
||||||
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>-->
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Environment config (copied manually by build script) -->
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace DeepDrftCli.Models
|
|
||||||
{
|
|
||||||
public class CliSettings
|
|
||||||
{
|
|
||||||
public string ConnectionString { get; set; } = string.Empty;
|
|
||||||
public string VaultPath { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using DeepDrftData;
|
|
||||||
using DeepDrftData.Data;
|
|
||||||
using DeepDrftData.Repositories;
|
|
||||||
using DeepDrftContent.Data.FileDatabase.Services;
|
|
||||||
using DeepDrftContent.Data.Processors;
|
|
||||||
using DeepDrftCli.Services;
|
|
||||||
using DeepDrftCli.Models;
|
|
||||||
using NetBlocks.Utilities.Environment;
|
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
|
||||||
|
|
||||||
// Load configuration from environment/config.json
|
|
||||||
var connectionsPath = CredentialTools.ResolvePathOrThrow(
|
|
||||||
"connections",
|
|
||||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "environment", "connections.json"));
|
|
||||||
builder.Configuration.AddJsonFile(connectionsPath, optional: false, reloadOnChange: false);
|
|
||||||
var cliSettings = builder.Configuration.GetSection(nameof(CliSettings)).Get<CliSettings>();
|
|
||||||
if (cliSettings is null) { throw new Exception("CLI settings are not configured"); }
|
|
||||||
|
|
||||||
// Add logging
|
|
||||||
builder.Services.AddLogging(configure => configure.AddConsole());
|
|
||||||
|
|
||||||
// Add database context
|
|
||||||
builder.Services.AddDbContext<DeepDrftContext>(options =>
|
|
||||||
options.UseNpgsql(cliSettings.ConnectionString));
|
|
||||||
|
|
||||||
// Add FileDatabase
|
|
||||||
builder.Services.AddSingleton<FileDatabase>(provider =>
|
|
||||||
{
|
|
||||||
var logger = provider.GetRequiredService<ILogger<Program>>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileDatabase = FileDatabase.FromAsync(cliSettings.VaultPath).GetAwaiter().GetResult();
|
|
||||||
if (fileDatabase == null)
|
|
||||||
{
|
|
||||||
logger.LogError("Failed to initialize FileDatabase");
|
|
||||||
throw new InvalidOperationException("FileDatabase initialization failed");
|
|
||||||
}
|
|
||||||
return fileDatabase;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error initializing FileDatabase");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add services. TrackManager fronts the BlazorBlocks data layer and implements
|
|
||||||
// ITrackService for legacy consumers; same scoped instance backs both registrations.
|
|
||||||
builder.Services.AddScoped<TrackRepository>();
|
|
||||||
builder.Services.AddScoped<TrackManager>();
|
|
||||||
builder.Services.AddScoped<ITrackService>(sp => sp.GetRequiredService<TrackManager>());
|
|
||||||
builder.Services.AddScoped<AudioProcessor>();
|
|
||||||
builder.Services.AddScoped<DeepDrftContent.Data.TrackService>();
|
|
||||||
builder.Services.AddScoped<CliService>();
|
|
||||||
builder.Services.AddScoped<GuiService>();
|
|
||||||
|
|
||||||
// Build and run
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Check if GUI mode is requested
|
|
||||||
if (args.Length > 0 && (args[0].ToLowerInvariant() == "gui" || args[0].ToLowerInvariant() == "--gui"))
|
|
||||||
{
|
|
||||||
// Run GUI mode
|
|
||||||
var guiService = app.Services.GetRequiredService<GuiService>();
|
|
||||||
await guiService.RunAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Run traditional CLI mode
|
|
||||||
var cliService = app.Services.GetRequiredService<CliService>();
|
|
||||||
await cliService.RunAsync(args);
|
|
||||||
}
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using DeepDrftContent.Data;
|
|
||||||
using DeepDrftModels.Entities;
|
|
||||||
using NetBlocks.Models;
|
|
||||||
using DeepDrftCli.Utils;
|
|
||||||
|
|
||||||
namespace DeepDrftCli.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main CLI service for handling command-line operations
|
|
||||||
/// </summary>
|
|
||||||
public class CliService
|
|
||||||
{
|
|
||||||
private readonly ILogger<CliService> _logger;
|
|
||||||
private readonly DeepDrftData.ITrackService _webTrackService;
|
|
||||||
private readonly DeepDrftContent.Data.TrackService _contentTrackService;
|
|
||||||
|
|
||||||
public CliService(
|
|
||||||
ILogger<CliService> logger,
|
|
||||||
DeepDrftData.ITrackService webTrackService,
|
|
||||||
DeepDrftContent.Data.TrackService contentTrackService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_webTrackService = webTrackService;
|
|
||||||
_contentTrackService = contentTrackService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main entry point for CLI operations
|
|
||||||
/// </summary>
|
|
||||||
public async Task RunAsync(string[] args)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (args.Length == 0)
|
|
||||||
{
|
|
||||||
ShowHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var command = args[0].ToLowerInvariant();
|
|
||||||
switch (command)
|
|
||||||
{
|
|
||||||
case "add":
|
|
||||||
await HandleAddCommand(args);
|
|
||||||
break;
|
|
||||||
case "list":
|
|
||||||
await HandleListCommand();
|
|
||||||
break;
|
|
||||||
case "gui":
|
|
||||||
case "--gui":
|
|
||||||
Console.WriteLine("Error: GUI mode should be launched directly. Use: DeepDrftCli gui");
|
|
||||||
break;
|
|
||||||
case "help":
|
|
||||||
case "--help":
|
|
||||||
case "-h":
|
|
||||||
ShowHelp();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Console.WriteLine($"Unknown command: {command}");
|
|
||||||
ShowHelp();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "CLI operation failed");
|
|
||||||
Console.WriteLine($"Error: {ex.Message}");
|
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the add command to add a new track
|
|
||||||
/// </summary>
|
|
||||||
private async Task HandleAddCommand(string[] args)
|
|
||||||
{
|
|
||||||
// Check if we have at least the command and file path
|
|
||||||
if (args.Length < 2)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Error: WAV file path is required.");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Usage: DeepDrftCli add <wav-file-path> [-i|--interactive] [track-name] [artist] [album] [genre] [release-date]");
|
|
||||||
Console.WriteLine(" DeepDrftCli add <wav-file-path> -i (interactive mode)");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\"");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var wavFilePath = args[1];
|
|
||||||
|
|
||||||
// Validate that the file path is not a flag
|
|
||||||
if (wavFilePath.StartsWith("-"))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Error: WAV file path is required and cannot be a flag.");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Usage: DeepDrftCli add <wav-file-path> [-i|--interactive] [track-name] [artist] [album] [genre] [release-date]");
|
|
||||||
Console.WriteLine(" DeepDrftCli add <wav-file-path> -i (interactive mode)");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\"");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isInteractive = args.Contains("-i") || args.Contains("--interactive");
|
|
||||||
|
|
||||||
// Filter out the interactive flags from args for processing
|
|
||||||
var filteredArgs = args.Where(arg => arg != "-i" && arg != "--interactive").ToArray();
|
|
||||||
|
|
||||||
string trackName;
|
|
||||||
string artist;
|
|
||||||
string? album;
|
|
||||||
string? genre;
|
|
||||||
DateOnly? releaseDate = null;
|
|
||||||
|
|
||||||
if (isInteractive)
|
|
||||||
{
|
|
||||||
// Interactive mode - prompt for metadata
|
|
||||||
var metadata = PromptForMetadata(wavFilePath, filteredArgs);
|
|
||||||
trackName = metadata.TrackName;
|
|
||||||
artist = metadata.Artist;
|
|
||||||
album = metadata.Album;
|
|
||||||
genre = metadata.Genre;
|
|
||||||
releaseDate = metadata.ReleaseDate;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Traditional command-line mode
|
|
||||||
if (filteredArgs.Length < 4)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Usage: DeepDrftCli add <wav-file-path> <track-name> <artist> [album] [genre] [release-date]");
|
|
||||||
Console.WriteLine(" DeepDrftCli add <wav-file-path> -i (interactive mode)");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist Name\" \"Album Name\" \"Rock\" \"2024-01-01\"");
|
|
||||||
Console.WriteLine("Example: DeepDrftCli add \"song.wav\" --interactive");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackName = filteredArgs[2];
|
|
||||||
artist = filteredArgs[3];
|
|
||||||
album = filteredArgs.Length > 4 ? filteredArgs[4] : null;
|
|
||||||
genre = filteredArgs.Length > 5 ? filteredArgs[5] : null;
|
|
||||||
|
|
||||||
if (filteredArgs.Length > 6 && DateOnly.TryParse(filteredArgs[6], out var parsedDate))
|
|
||||||
{
|
|
||||||
releaseDate = parsedDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"Adding track: {trackName} by {artist}");
|
|
||||||
Console.WriteLine($"Processing WAV file: {wavFilePath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Initialize tracks vault if needed
|
|
||||||
await _contentTrackService.InitializeTracksVaultAsync();
|
|
||||||
|
|
||||||
// Add track to FileDatabase and get entity
|
|
||||||
var trackEntity = await _contentTrackService.AddTrackFromWavAsync(
|
|
||||||
wavFilePath, trackName, artist, album, genre, releaseDate);
|
|
||||||
|
|
||||||
if (trackEntity == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Failed to process audio file");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add track to SQL database
|
|
||||||
var result = await _webTrackService.Create(trackEntity);
|
|
||||||
if (result.Success && result.Value != null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"✓ Track added successfully!");
|
|
||||||
Console.WriteLine($" ID: {result.Value.Id}");
|
|
||||||
Console.WriteLine($" Name: {result.Value.TrackName}");
|
|
||||||
Console.WriteLine($" Artist: {result.Value.Artist}");
|
|
||||||
Console.WriteLine($" Album: {result.Value.Album ?? "N/A"}");
|
|
||||||
Console.WriteLine($" Genre: {result.Value.Genre ?? "N/A"}");
|
|
||||||
Console.WriteLine($" Release Date: {result.Value.ReleaseDate?.ToString() ?? "N/A"}");
|
|
||||||
Console.WriteLine($" Entry Key: {result.Value.EntryKey}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
Console.WriteLine($"Failed to save track to database: {errorMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: WAV file not found: {wavFilePath}");
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: {ex.Message}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error adding track: {ex.Message}");
|
|
||||||
_logger.LogError(ex, "Failed to add track");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles the list command to show all tracks
|
|
||||||
/// </summary>
|
|
||||||
private async Task HandleListCommand()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("Retrieving tracks from database...");
|
|
||||||
|
|
||||||
var result = await _webTrackService.GetAll();
|
|
||||||
if (!result.Success || result.Value == null)
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
Console.WriteLine($"Failed to retrieve tracks: {errorMessage}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tracks = result.Value;
|
|
||||||
if (tracks.Count == 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine("No tracks found in database.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"\nFound {tracks.Count} tracks:");
|
|
||||||
Console.WriteLine(new string('-', 80));
|
|
||||||
Console.WriteLine($"{"ID",-5} {"Name",-25} {"Artist",-20} {"Album",-15} {"Genre",-10}");
|
|
||||||
Console.WriteLine(new string('-', 80));
|
|
||||||
|
|
||||||
foreach (var track in tracks)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"{track.Id,-5} {CliUtils.TruncateString(track.TrackName, 25),-25} {CliUtils.TruncateString(track.Artist, 20),-20} {CliUtils.TruncateString(track.Album ?? "", 15),-15} {CliUtils.TruncateString(track.Genre ?? "", 10),-10}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error listing tracks: {ex.Message}");
|
|
||||||
_logger.LogError(ex, "Failed to list tracks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shows help information
|
|
||||||
/// </summary>
|
|
||||||
private void ShowHelp()
|
|
||||||
{
|
|
||||||
Console.WriteLine("DeepDrft CLI - Audio Track Management Tool");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Usage:");
|
|
||||||
Console.WriteLine(" DeepDrftCli gui - Launch interactive GUI mode");
|
|
||||||
Console.WriteLine(" DeepDrftCli [command] [options] - Run command-line mode");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Commands:");
|
|
||||||
Console.WriteLine(" add <wav-file> <track-name> <artist> [album] [genre] [release-date]");
|
|
||||||
Console.WriteLine(" - Adds a WAV file to both SQL and FileDatabase");
|
|
||||||
Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" \"My Song\" \"Artist\" \"Album\" \"Rock\" \"2024-01-01\"");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" add <wav-file> -i|--interactive [track-name] [artist] [album] [genre] [release-date]");
|
|
||||||
Console.WriteLine(" - Adds a WAV file with interactive metadata prompts");
|
|
||||||
Console.WriteLine(" - Any provided command-line arguments will be used as defaults");
|
|
||||||
Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" -i");
|
|
||||||
Console.WriteLine(" - Example: DeepDrftCli add \"song.wav\" --interactive \"My Song\"");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" list");
|
|
||||||
Console.WriteLine(" - Lists all tracks in the database");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" help");
|
|
||||||
Console.WriteLine(" - Shows this help information");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Interactive Mode Features:");
|
|
||||||
Console.WriteLine(" - Prompts for each metadata field individually");
|
|
||||||
Console.WriteLine(" - Shows file name being processed");
|
|
||||||
Console.WriteLine(" - Supports default values and fallback to command-line args");
|
|
||||||
Console.WriteLine(" - Required fields: Track Name, Artist");
|
|
||||||
Console.WriteLine(" - Optional fields: Album, Genre, Release Date");
|
|
||||||
Console.WriteLine(" - Summary confirmation before proceeding");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Notes:");
|
|
||||||
Console.WriteLine(" - Only WAV files are supported");
|
|
||||||
Console.WriteLine(" - Release date format: YYYY-MM-DD");
|
|
||||||
Console.WriteLine(" - Arguments with spaces should be quoted");
|
|
||||||
Console.WriteLine(" - Use * to indicate required fields in interactive mode");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prompts user for track metadata interactively
|
|
||||||
/// </summary>
|
|
||||||
private TrackMetadata PromptForMetadata(string wavFilePath, string[] args)
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("=== Interactive Metadata Entry ===");
|
|
||||||
Console.WriteLine($"Processing file: {Path.GetFileName(wavFilePath)}");
|
|
||||||
Console.WriteLine("Press Enter to use default values or skip optional fields.");
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
// Check if any metadata was provided via command line (fallback support)
|
|
||||||
var trackName = args.Length > 2 ? args[2] : null;
|
|
||||||
var artist = args.Length > 3 ? args[3] : null;
|
|
||||||
var album = args.Length > 4 ? args[4] : null;
|
|
||||||
var genre = args.Length > 5 ? args[5] : null;
|
|
||||||
DateOnly? releaseDate = null;
|
|
||||||
if (args.Length > 6 && DateOnly.TryParse(args[6], out var parsedDate))
|
|
||||||
releaseDate = parsedDate;
|
|
||||||
|
|
||||||
// Prompt for track name (required)
|
|
||||||
trackName ??= PromptForInput("Track Name", required: true);
|
|
||||||
|
|
||||||
// Prompt for artist (required)
|
|
||||||
artist ??= PromptForInput("Artist", required: true);
|
|
||||||
|
|
||||||
// Prompt for album (optional)
|
|
||||||
album ??= PromptForInput("Album", defaultValue: album);
|
|
||||||
|
|
||||||
// Prompt for genre (optional)
|
|
||||||
genre ??= PromptForInput("Genre", defaultValue: genre);
|
|
||||||
|
|
||||||
// Prompt for release date (optional)
|
|
||||||
if (releaseDate == null)
|
|
||||||
{
|
|
||||||
var releaseDateInput = PromptForInput("Release Date (YYYY-MM-DD)", defaultValue: releaseDate?.ToString());
|
|
||||||
if (!string.IsNullOrWhiteSpace(releaseDateInput) && DateOnly.TryParse(releaseDateInput, out var newReleaseDate))
|
|
||||||
{
|
|
||||||
releaseDate = newReleaseDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("=== Summary ===");
|
|
||||||
Console.WriteLine($"Track Name: {trackName}");
|
|
||||||
Console.WriteLine($"Artist: {artist}");
|
|
||||||
Console.WriteLine($"Album: {album ?? "N/A"}");
|
|
||||||
Console.WriteLine($"Genre: {genre ?? "N/A"}");
|
|
||||||
Console.WriteLine($"Release Date: {releaseDate?.ToString() ?? "N/A"}");
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
if (!ConfirmProceed("Proceed with these details?"))
|
|
||||||
{
|
|
||||||
Console.WriteLine("Operation cancelled.");
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TrackMetadata
|
|
||||||
{
|
|
||||||
TrackName = trackName,
|
|
||||||
Artist = artist,
|
|
||||||
Album = album,
|
|
||||||
Genre = genre,
|
|
||||||
ReleaseDate = releaseDate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prompts user for a single input field
|
|
||||||
/// </summary>
|
|
||||||
private string PromptForInput(string fieldName, bool required = false, string? defaultValue = null)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var prompt = $"{fieldName}";
|
|
||||||
if (!string.IsNullOrWhiteSpace(defaultValue))
|
|
||||||
prompt += $" [{defaultValue}]";
|
|
||||||
if (required)
|
|
||||||
prompt += " *";
|
|
||||||
prompt += ": ";
|
|
||||||
|
|
||||||
Console.Write(prompt);
|
|
||||||
var input = Console.ReadLine();
|
|
||||||
|
|
||||||
// Handle null input (Ctrl+C scenario)
|
|
||||||
if (input == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine("Operation cancelled.");
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
input = input.Trim();
|
|
||||||
|
|
||||||
// Use default value if input is empty and default exists
|
|
||||||
if (string.IsNullOrWhiteSpace(input) && !string.IsNullOrWhiteSpace(defaultValue))
|
|
||||||
return defaultValue;
|
|
||||||
|
|
||||||
// If not required and input is empty, return empty string
|
|
||||||
if (!required && string.IsNullOrWhiteSpace(input))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
// Check if required field is provided
|
|
||||||
if (required && string.IsNullOrWhiteSpace(input))
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error: {fieldName} is required. Please provide a value.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prompts user for yes/no confirmation
|
|
||||||
/// </summary>
|
|
||||||
private bool ConfirmProceed(string message)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
Console.Write($"{message} (y/n): ");
|
|
||||||
var input = Console.ReadKey();
|
|
||||||
Console.WriteLine();
|
|
||||||
|
|
||||||
switch (input.KeyChar.ToString().ToLowerInvariant())
|
|
||||||
{
|
|
||||||
case "y":
|
|
||||||
return true;
|
|
||||||
case "n":
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
Console.WriteLine("Please enter 'y' for yes or 'n' for no.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents track metadata for interactive input
|
|
||||||
/// </summary>
|
|
||||||
private record TrackMetadata
|
|
||||||
{
|
|
||||||
public required string TrackName { get; init; }
|
|
||||||
public required string Artist { get; init; }
|
|
||||||
public string? Album { get; init; }
|
|
||||||
public string? Genre { get; init; }
|
|
||||||
public DateOnly? ReleaseDate { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,898 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Terminal.Gui;
|
|
||||||
using DeepDrftModels.Entities;
|
|
||||||
using DeepDrftCli.Utils;
|
|
||||||
|
|
||||||
namespace DeepDrftCli.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Terminal.Gui based interactive interface for DeepDrft CLI operations
|
|
||||||
/// </summary>
|
|
||||||
public class GuiService
|
|
||||||
{
|
|
||||||
private readonly ILogger<GuiService> _logger;
|
|
||||||
private readonly DeepDrftData.ITrackService _webTrackService;
|
|
||||||
private readonly DeepDrftContent.Data.TrackService _contentTrackService;
|
|
||||||
|
|
||||||
// GUI Components
|
|
||||||
private Window? _mainWindow;
|
|
||||||
private MenuBar? _menuBar;
|
|
||||||
private ListView? _trackListView;
|
|
||||||
private TextView? _statusView;
|
|
||||||
private FrameView? _legendFrame;
|
|
||||||
private List<TrackEntity> _tracks = new();
|
|
||||||
|
|
||||||
public GuiService(
|
|
||||||
ILogger<GuiService> logger,
|
|
||||||
DeepDrftData.ITrackService webTrackService,
|
|
||||||
DeepDrftContent.Data.TrackService contentTrackService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_webTrackService = webTrackService;
|
|
||||||
_contentTrackService = contentTrackService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize and run the GUI application
|
|
||||||
/// </summary>
|
|
||||||
public async Task RunAsync()
|
|
||||||
{
|
|
||||||
Application.Init();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await SetupMainWindowAsync();
|
|
||||||
Application.Run(_mainWindow);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "GUI application failed");
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Error", $"Application failed: {ex.Message}", "OK");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Application.Shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup the main application window with all components
|
|
||||||
/// </summary>
|
|
||||||
private async Task SetupMainWindowAsync()
|
|
||||||
{
|
|
||||||
// Create main window with DeepDrft theme
|
|
||||||
_mainWindow = new Window("DeepDrft CLI - Interactive Mode")
|
|
||||||
{
|
|
||||||
X = 0,
|
|
||||||
Y = 1, // Leave room for menu bar
|
|
||||||
Width = Dim.Fill(),
|
|
||||||
Height = Dim.Fill()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply DeepDrft theme to main window with improved contrast
|
|
||||||
_mainWindow.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.White, Color.Black),
|
|
||||||
Focus = new Terminal.Gui.Attribute(Color.White, Color.DarkGray),
|
|
||||||
HotNormal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black),
|
|
||||||
HotFocus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.DarkGray)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup menu bar
|
|
||||||
SetupMenuBar();
|
|
||||||
|
|
||||||
// Setup track list view
|
|
||||||
SetupTrackListView();
|
|
||||||
|
|
||||||
// Setup status view
|
|
||||||
SetupStatusView();
|
|
||||||
|
|
||||||
// Setup legend panel
|
|
||||||
SetupLegendPanel();
|
|
||||||
|
|
||||||
// Setup key bindings
|
|
||||||
SetupKeyBindings();
|
|
||||||
|
|
||||||
// Add components to main window
|
|
||||||
_mainWindow.Add(_trackListView!, _statusView!, _legendFrame!);
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
await RefreshTrackListAsync();
|
|
||||||
|
|
||||||
// Set initial status
|
|
||||||
UpdateStatus("Ready - Use keyboard shortcuts shown below or press F1 for detailed help");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup the menu bar with color-coded options
|
|
||||||
/// </summary>
|
|
||||||
private void SetupMenuBar()
|
|
||||||
{
|
|
||||||
_menuBar = new MenuBar(new MenuBarItem[] {
|
|
||||||
new MenuBarItem("_File", new MenuItem[] {
|
|
||||||
new MenuItem("_Add Track (Ctrl+A)", "", () => ShowAddTrackDialog()),
|
|
||||||
new MenuItem("_Refresh (F5)", "", async () => await RefreshTrackListAsync()),
|
|
||||||
null, // Separator
|
|
||||||
new MenuItem("_Quit (Ctrl+Q)", "", () => Application.RequestStop())
|
|
||||||
}),
|
|
||||||
new MenuBarItem("_Edit", new MenuItem[] {
|
|
||||||
new MenuItem("_Edit Track (Ctrl+E)", "", () => ShowEditTrackDialog()),
|
|
||||||
new MenuItem("_Delete Track (Delete)", "", () => ShowDeleteTrackDialog()),
|
|
||||||
null, // Separator
|
|
||||||
new MenuItem("_Track Details (Enter)", "", () => ShowTrackDetails()),
|
|
||||||
}),
|
|
||||||
new MenuBarItem("_View", new MenuItem[] {
|
|
||||||
new MenuItem("_Clear Status (Ctrl+L)", "", () => ClearStatus()),
|
|
||||||
}),
|
|
||||||
new MenuBarItem("_Help", new MenuItem[] {
|
|
||||||
new MenuItem("_Shortcuts (F1)", "", () => ShowHelp()),
|
|
||||||
new MenuItem("_About", "", () => ShowAbout())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
Application.Top.Add(_menuBar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup the track list view with color coding
|
|
||||||
/// </summary>
|
|
||||||
private void SetupTrackListView()
|
|
||||||
{
|
|
||||||
_trackListView = new ListView()
|
|
||||||
{
|
|
||||||
X = 0,
|
|
||||||
Y = 0,
|
|
||||||
Width = Dim.Fill(),
|
|
||||||
Height = Dim.Fill(6), // Leave room for status and legend at bottom
|
|
||||||
CanFocus = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up high-contrast colors for track list visibility
|
|
||||||
_trackListView.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.White, Color.Black),
|
|
||||||
Focus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.Blue), // High contrast selection
|
|
||||||
HotNormal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black),
|
|
||||||
HotFocus = new Terminal.Gui.Attribute(Color.White, Color.Blue) // Clear cursor visibility
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle selection events
|
|
||||||
_trackListView.SelectedItemChanged += OnTrackSelectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup the status view at the bottom
|
|
||||||
/// </summary>
|
|
||||||
private void SetupStatusView()
|
|
||||||
{
|
|
||||||
_statusView = new TextView()
|
|
||||||
{
|
|
||||||
X = 0,
|
|
||||||
Y = Pos.AnchorEnd(5), // Position above legend panel
|
|
||||||
Width = Dim.Fill(),
|
|
||||||
Height = 2,
|
|
||||||
ReadOnly = true,
|
|
||||||
WordWrap = true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Status view with high contrast colors for better readability
|
|
||||||
_statusView.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black), // High contrast status
|
|
||||||
Focus = new Terminal.Gui.Attribute(Color.BrightCyan, Color.DarkGray)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup the hotkey legend panel at the bottom
|
|
||||||
/// </summary>
|
|
||||||
private void SetupLegendPanel()
|
|
||||||
{
|
|
||||||
_legendFrame = new FrameView("Keyboard Shortcuts")
|
|
||||||
{
|
|
||||||
X = 0,
|
|
||||||
Y = Pos.AnchorEnd(3),
|
|
||||||
Width = Dim.Fill(),
|
|
||||||
Height = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply high contrast theme to legend frame for better visibility
|
|
||||||
_legendFrame.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.White, Color.Black), // Clear border
|
|
||||||
Focus = new Terminal.Gui.Attribute(Color.White, Color.DarkGray),
|
|
||||||
HotNormal = new Terminal.Gui.Attribute(Color.BrightYellow, Color.Black),
|
|
||||||
HotFocus = new Terminal.Gui.Attribute(Color.BrightYellow, Color.DarkGray)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create legend content with color coding
|
|
||||||
var legendView = new TextView()
|
|
||||||
{
|
|
||||||
X = 0,
|
|
||||||
Y = 0,
|
|
||||||
Width = Dim.Fill(),
|
|
||||||
Height = Dim.Fill(),
|
|
||||||
ReadOnly = true,
|
|
||||||
WordWrap = false,
|
|
||||||
Text = CreateLegendText()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Legend with high contrast colors for easy reading
|
|
||||||
legendView.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightGreen, Color.Black), // High contrast legend text
|
|
||||||
Focus = new Terminal.Gui.Attribute(Color.BrightGreen, Color.DarkGray)
|
|
||||||
};
|
|
||||||
|
|
||||||
_legendFrame.Add(legendView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create the formatted legend text with shortcuts
|
|
||||||
/// </summary>
|
|
||||||
private string CreateLegendText()
|
|
||||||
{
|
|
||||||
return "Ctrl+A: Add │ Ctrl+E: Edit │ Del: Delete │ F5: Refresh │ Enter: Details │ F1: Help │ Ctrl+Q: Quit";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Setup keyboard shortcuts
|
|
||||||
/// </summary>
|
|
||||||
private void SetupKeyBindings()
|
|
||||||
{
|
|
||||||
// Global key bindings using KeyDown event which is more reliable
|
|
||||||
_mainWindow.KeyDown += async (e) =>
|
|
||||||
{
|
|
||||||
var key = e.KeyEvent.Key;
|
|
||||||
|
|
||||||
// Debug logging for key presses
|
|
||||||
UpdateStatus($"Key pressed: {key}");
|
|
||||||
|
|
||||||
switch (key)
|
|
||||||
{
|
|
||||||
case Key.CtrlMask | Key.Q:
|
|
||||||
case Key.CtrlMask | Key.q:
|
|
||||||
Application.RequestStop();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.CtrlMask | Key.A:
|
|
||||||
case Key.CtrlMask | Key.a:
|
|
||||||
UpdateStatus("Opening Add Track dialog...");
|
|
||||||
ShowAddTrackDialog();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.CtrlMask | Key.E:
|
|
||||||
case Key.CtrlMask | Key.e:
|
|
||||||
UpdateStatus("Opening Edit Track dialog...");
|
|
||||||
ShowEditTrackDialog();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.DeleteChar:
|
|
||||||
UpdateStatus("Opening Delete Track dialog...");
|
|
||||||
ShowDeleteTrackDialog();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.F5:
|
|
||||||
UpdateStatus("Refreshing track list...");
|
|
||||||
await RefreshTrackListAsync();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.F1:
|
|
||||||
ShowHelp();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.CtrlMask | Key.L:
|
|
||||||
case Key.CtrlMask | Key.l:
|
|
||||||
ClearStatus();
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Key.Enter:
|
|
||||||
if (_trackListView?.HasFocus == true)
|
|
||||||
{
|
|
||||||
ShowTrackDetails();
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Also add global application-level key bindings as backup
|
|
||||||
Application.Top.KeyDown += async (e) =>
|
|
||||||
{
|
|
||||||
var key = e.KeyEvent.Key;
|
|
||||||
|
|
||||||
switch (key)
|
|
||||||
{
|
|
||||||
case Key.CtrlMask | Key.A:
|
|
||||||
case Key.CtrlMask | Key.a:
|
|
||||||
if (!e.Handled)
|
|
||||||
{
|
|
||||||
UpdateStatus("Global: Opening Add Track dialog...");
|
|
||||||
ShowAddTrackDialog();
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show the Add Track dialog with form validation
|
|
||||||
/// </summary>
|
|
||||||
private void ShowAddTrackDialog()
|
|
||||||
{
|
|
||||||
var dialog = new Dialog("Add New Track", 80, 18);
|
|
||||||
|
|
||||||
// File path field
|
|
||||||
var filePathLabel = new Label("WAV File Path:") { X = 1, Y = 1 };
|
|
||||||
var filePathField = new TextField("") { X = 1, Y = 2, Width = Dim.Fill(2) };
|
|
||||||
var browseButton = new Button("Browse...") { X = Pos.AnchorEnd(12), Y = 2 };
|
|
||||||
|
|
||||||
// Track metadata fields
|
|
||||||
var trackNameLabel = new Label("Track Name: *") { X = 1, Y = 4 };
|
|
||||||
var trackNameField = new TextField("") { X = 1, Y = 5, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var artistLabel = new Label("Artist: *") { X = 1, Y = 6 };
|
|
||||||
var artistField = new TextField("") { X = 1, Y = 7, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var albumLabel = new Label("Album:") { X = 1, Y = 8 };
|
|
||||||
var albumField = new TextField("") { X = 1, Y = 9, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var genreLabel = new Label("Genre:") { X = 1, Y = 10 };
|
|
||||||
var genreField = new TextField("") { X = 1, Y = 11, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var releaseDateLabel = new Label("Release Date (YYYY-MM-DD):") { X = 1, Y = 12 };
|
|
||||||
var releaseDateField = new TextField("") { X = 1, Y = 13, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
var addButton = new Button("Add Track") { X = 1, Y = 15 };
|
|
||||||
var cancelButton = new Button("Cancel") { X = 15, Y = 15 };
|
|
||||||
|
|
||||||
// Color coding for required fields with high contrast
|
|
||||||
trackNameLabel.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) // High contrast for required fields
|
|
||||||
};
|
|
||||||
artistLabel.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black) // High contrast for required fields
|
|
||||||
};
|
|
||||||
|
|
||||||
// Browse button click handler
|
|
||||||
browseButton.Clicked += () =>
|
|
||||||
{
|
|
||||||
var openDialog = new OpenDialog("Select WAV File", "Select a WAV audio file to add");
|
|
||||||
openDialog.AllowedFileTypes = new[] { ".wav" };
|
|
||||||
|
|
||||||
Application.Run(openDialog);
|
|
||||||
|
|
||||||
if (!openDialog.Canceled && openDialog.FilePath != null)
|
|
||||||
{
|
|
||||||
filePathField.Text = openDialog.FilePath.ToString();
|
|
||||||
|
|
||||||
// Try to extract metadata from filename
|
|
||||||
var fileName = Path.GetFileNameWithoutExtension(openDialog.FilePath.ToString());
|
|
||||||
if (string.IsNullOrEmpty(trackNameField.Text.ToString()))
|
|
||||||
{
|
|
||||||
trackNameField.Text = fileName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add button click handler
|
|
||||||
addButton.Clicked += async () =>
|
|
||||||
{
|
|
||||||
if (await ValidateAndAddTrackAsync(
|
|
||||||
filePathField.Text.ToString(),
|
|
||||||
trackNameField.Text.ToString(),
|
|
||||||
artistField.Text.ToString(),
|
|
||||||
albumField.Text.ToString(),
|
|
||||||
genreField.Text.ToString(),
|
|
||||||
releaseDateField.Text.ToString()))
|
|
||||||
{
|
|
||||||
Application.RequestStop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cancel button click handler
|
|
||||||
cancelButton.Clicked += () => Application.RequestStop();
|
|
||||||
|
|
||||||
// Add all components to dialog
|
|
||||||
dialog.Add(filePathLabel, filePathField, browseButton,
|
|
||||||
trackNameLabel, trackNameField,
|
|
||||||
artistLabel, artistField,
|
|
||||||
albumLabel, albumField,
|
|
||||||
genreLabel, genreField,
|
|
||||||
releaseDateLabel, releaseDateField,
|
|
||||||
addButton, cancelButton);
|
|
||||||
|
|
||||||
// Set focus to file path field
|
|
||||||
filePathField.SetFocus();
|
|
||||||
|
|
||||||
Application.Run(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show the Edit Track dialog for the selected track
|
|
||||||
/// </summary>
|
|
||||||
private void ShowEditTrackDialog()
|
|
||||||
{
|
|
||||||
if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count)
|
|
||||||
{
|
|
||||||
UpdateStatus("No track selected for editing. Select a track first.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTrack = _tracks[_trackListView!.SelectedItem];
|
|
||||||
|
|
||||||
var dialog = new Dialog("Edit Track", 80, 18);
|
|
||||||
|
|
||||||
// Track metadata fields pre-filled with current values
|
|
||||||
var trackNameLabel = new Label("Track Name: *") { X = 1, Y = 1 };
|
|
||||||
var trackNameField = new TextField(selectedTrack.TrackName) { X = 1, Y = 2, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var artistLabel = new Label("Artist: *") { X = 1, Y = 3 };
|
|
||||||
var artistField = new TextField(selectedTrack.Artist) { X = 1, Y = 4, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var albumLabel = new Label("Album:") { X = 1, Y = 5 };
|
|
||||||
var albumField = new TextField(selectedTrack.Album ?? "") { X = 1, Y = 6, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var genreLabel = new Label("Genre:") { X = 1, Y = 7 };
|
|
||||||
var genreField = new TextField(selectedTrack.Genre ?? "") { X = 1, Y = 8, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
var releaseDateLabel = new Label("Release Date (YYYY-MM-DD):") { X = 1, Y = 9 };
|
|
||||||
var releaseDateField = new TextField(selectedTrack.ReleaseDate?.ToString() ?? "") { X = 1, Y = 10, Width = Dim.Fill(2) };
|
|
||||||
|
|
||||||
// Info label showing current track ID
|
|
||||||
var infoLabel = new Label($"Editing Track ID: {selectedTrack.Id} - Entry Key: {selectedTrack.EntryKey}") { X = 1, Y = 12 };
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
var saveButton = new Button("Save Changes") { X = 1, Y = 14 };
|
|
||||||
var cancelButton = new Button("Cancel") { X = 18, Y = 14 };
|
|
||||||
|
|
||||||
// Color coding for required fields with high contrast
|
|
||||||
trackNameLabel.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black)
|
|
||||||
};
|
|
||||||
artistLabel.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightRed, Color.Black)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Info label styling with better contrast
|
|
||||||
infoLabel.ColorScheme = new ColorScheme()
|
|
||||||
{
|
|
||||||
Normal = new Terminal.Gui.Attribute(Color.BrightCyan, Color.Black)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save button click handler
|
|
||||||
saveButton.Clicked += async () =>
|
|
||||||
{
|
|
||||||
if (await ValidateAndUpdateTrackAsync(selectedTrack,
|
|
||||||
trackNameField.Text.ToString(),
|
|
||||||
artistField.Text.ToString(),
|
|
||||||
albumField.Text.ToString(),
|
|
||||||
genreField.Text.ToString(),
|
|
||||||
releaseDateField.Text.ToString()))
|
|
||||||
{
|
|
||||||
Application.RequestStop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cancel button click handler
|
|
||||||
cancelButton.Clicked += () => Application.RequestStop();
|
|
||||||
|
|
||||||
// Add all components to dialog
|
|
||||||
dialog.Add(trackNameLabel, trackNameField,
|
|
||||||
artistLabel, artistField,
|
|
||||||
albumLabel, albumField,
|
|
||||||
genreLabel, genreField,
|
|
||||||
releaseDateLabel, releaseDateField,
|
|
||||||
infoLabel,
|
|
||||||
saveButton, cancelButton);
|
|
||||||
|
|
||||||
// Set focus to track name field
|
|
||||||
trackNameField.SetFocus();
|
|
||||||
|
|
||||||
Application.Run(dialog);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show the Delete Track confirmation dialog for the selected track
|
|
||||||
/// </summary>
|
|
||||||
private void ShowDeleteTrackDialog()
|
|
||||||
{
|
|
||||||
if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count)
|
|
||||||
{
|
|
||||||
UpdateStatus("No track selected for deletion. Select a track first.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTrack = _tracks[_trackListView!.SelectedItem];
|
|
||||||
|
|
||||||
var message = $"Are you sure you want to delete this track?\n\n" +
|
|
||||||
$"Track: {selectedTrack.TrackName}\n" +
|
|
||||||
$"Artist: {selectedTrack.Artist}\n" +
|
|
||||||
$"Album: {selectedTrack.Album ?? "N/A"}\n" +
|
|
||||||
$"Genre: {selectedTrack.Genre ?? "N/A"}\n" +
|
|
||||||
$"ID: {selectedTrack.Id}\n\n" +
|
|
||||||
$"WARNING: This action cannot be undone!\n" +
|
|
||||||
$"The track metadata will be removed from the database.";
|
|
||||||
|
|
||||||
var result = MessageBox.Query(70, 14, "Confirm Delete Track", message, "Delete", "Cancel");
|
|
||||||
|
|
||||||
if (result == 0) // Delete button clicked
|
|
||||||
{
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await DeleteTrackAsync(selectedTrack);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delete the specified track from the database
|
|
||||||
/// </summary>
|
|
||||||
private async Task DeleteTrackAsync(TrackEntity trackToDelete)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UpdateStatus($"Deleting track '{trackToDelete.TrackName}'...");
|
|
||||||
|
|
||||||
// Delete from SQL database
|
|
||||||
var result = await _webTrackService.Delete(trackToDelete.Id);
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
UpdateStatus($"✓ Track '{trackToDelete.TrackName}' by {trackToDelete.Artist} deleted successfully!");
|
|
||||||
await RefreshTrackListAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
UpdateStatus($"Failed to delete track: {errorMessage}");
|
|
||||||
|
|
||||||
// Show error dialog on UI thread
|
|
||||||
Application.MainLoop.Invoke(() =>
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to delete track: {errorMessage}", "OK");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to delete track via GUI");
|
|
||||||
UpdateStatus($"Error deleting track: {ex.Message}");
|
|
||||||
|
|
||||||
// Show error dialog on UI thread
|
|
||||||
Application.MainLoop.Invoke(() =>
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate input and add track to database
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> ValidateAndAddTrackAsync(string filePath, string trackName,
|
|
||||||
string artist, string album, string genre, string releaseDate)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate required fields
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Validation Error", "File path is required.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(trackName))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Validation Error", "Track name is required.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(artist))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Validation Error", "Artist is required.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file exists and has .wav extension
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "File Error", "The specified file does not exist.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Path.GetExtension(filePath).Equals(".wav", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "File Error", "Only WAV files are supported.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate release date if provided
|
|
||||||
DateOnly? parsedReleaseDate = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(releaseDate))
|
|
||||||
{
|
|
||||||
if (!DateOnly.TryParse(releaseDate, out var date))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Date Error", "Release date must be in YYYY-MM-DD format.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
parsedReleaseDate = date;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show progress dialog
|
|
||||||
UpdateStatus("Processing audio file...");
|
|
||||||
|
|
||||||
// Initialize tracks vault
|
|
||||||
await _contentTrackService.InitializeTracksVaultAsync();
|
|
||||||
|
|
||||||
// Process and add track
|
|
||||||
var trackEntity = await _contentTrackService.AddTrackFromWavAsync(
|
|
||||||
filePath, trackName, artist,
|
|
||||||
string.IsNullOrWhiteSpace(album) ? null : album,
|
|
||||||
string.IsNullOrWhiteSpace(genre) ? null : genre,
|
|
||||||
parsedReleaseDate);
|
|
||||||
|
|
||||||
if (trackEntity == null)
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Processing Error", "Failed to process audio file.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to SQL database
|
|
||||||
var result = await _webTrackService.Create(trackEntity);
|
|
||||||
if (result.Success && result.Value != null)
|
|
||||||
{
|
|
||||||
UpdateStatus($"✓ Track '{trackName}' by {artist} added successfully!");
|
|
||||||
await RefreshTrackListAsync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to save track: {errorMessage}", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to add track via GUI");
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate input and update existing track in database
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> ValidateAndUpdateTrackAsync(TrackEntity originalTrack, string trackName,
|
|
||||||
string artist, string album, string genre, string releaseDate)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Validate required fields
|
|
||||||
if (string.IsNullOrWhiteSpace(trackName))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Validation Error", "Track name is required.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(artist))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Validation Error", "Artist is required.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate release date if provided
|
|
||||||
DateOnly? parsedReleaseDate = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(releaseDate))
|
|
||||||
{
|
|
||||||
if (!DateOnly.TryParse(releaseDate, out var date))
|
|
||||||
{
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Date Error", "Release date must be in YYYY-MM-DD format.", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
parsedReleaseDate = date;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateStatus("Updating track...");
|
|
||||||
|
|
||||||
// Create updated track entity
|
|
||||||
var updatedTrack = new TrackEntity
|
|
||||||
{
|
|
||||||
Id = originalTrack.Id,
|
|
||||||
EntryKey = originalTrack.EntryKey, // Keep original entry key
|
|
||||||
TrackName = trackName,
|
|
||||||
Artist = artist,
|
|
||||||
Album = string.IsNullOrWhiteSpace(album) ? null : album,
|
|
||||||
Genre = string.IsNullOrWhiteSpace(genre) ? null : genre,
|
|
||||||
ReleaseDate = parsedReleaseDate,
|
|
||||||
ImagePath = originalTrack.ImagePath // Keep original image path
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update in SQL database
|
|
||||||
var result = await _webTrackService.Update(updatedTrack);
|
|
||||||
if (result.Success && result.Value != null)
|
|
||||||
{
|
|
||||||
UpdateStatus($"✓ Track '{trackName}' by {artist} updated successfully!");
|
|
||||||
await RefreshTrackListAsync();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Database Error", $"Failed to update track: {errorMessage}", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to update track via GUI");
|
|
||||||
MessageBox.ErrorQuery(60, 8, "Error", $"An error occurred: {ex.Message}", "OK");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show detailed information about the selected track
|
|
||||||
/// </summary>
|
|
||||||
private void ShowTrackDetails()
|
|
||||||
{
|
|
||||||
if (_trackListView?.SelectedItem < 0 || _trackListView?.SelectedItem >= _tracks.Count)
|
|
||||||
{
|
|
||||||
UpdateStatus("No track selected.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedTrack = _tracks[_trackListView!.SelectedItem];
|
|
||||||
|
|
||||||
var details = $"Track Details:\n\n" +
|
|
||||||
$"ID: {selectedTrack.Id}\n" +
|
|
||||||
$"Name: {selectedTrack.TrackName}\n" +
|
|
||||||
$"Artist: {selectedTrack.Artist}\n" +
|
|
||||||
$"Album: {selectedTrack.Album ?? "N/A"}\n" +
|
|
||||||
$"Genre: {selectedTrack.Genre ?? "N/A"}\n" +
|
|
||||||
$"Release Date: {selectedTrack.ReleaseDate?.ToString() ?? "N/A"}\n" +
|
|
||||||
$"Entry Key: {selectedTrack.EntryKey}\n" +
|
|
||||||
$"Image Path: {selectedTrack.ImagePath ?? "N/A"}";
|
|
||||||
|
|
||||||
MessageBox.Query(70, 12, "Track Details", details, "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refresh the track list from database
|
|
||||||
/// </summary>
|
|
||||||
private async Task RefreshTrackListAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
UpdateStatus("Loading tracks...");
|
|
||||||
|
|
||||||
var result = await _webTrackService.GetAll();
|
|
||||||
if (result.Success && result.Value != null)
|
|
||||||
{
|
|
||||||
_tracks = result.Value.ToList();
|
|
||||||
|
|
||||||
// Create display items for the list view
|
|
||||||
var displayItems = _tracks.Select(t =>
|
|
||||||
$"{t.Id,4} │ {CliUtils.TruncateString(t.TrackName, 25),25} │ {CliUtils.TruncateString(t.Artist, 20),20} │ {CliUtils.TruncateString(t.Album ?? "", 15),15} │ {CliUtils.TruncateString(t.Genre ?? "", 10),10}"
|
|
||||||
).ToArray();
|
|
||||||
|
|
||||||
_trackListView?.SetSource(displayItems);
|
|
||||||
|
|
||||||
UpdateStatus($"Loaded {_tracks.Count} tracks. Use shortcuts below or navigate with ↑/↓ arrows.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var errorMessage = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
||||||
UpdateStatus($"Failed to load tracks: {errorMessage}");
|
|
||||||
MessageBox.ErrorQuery(50, 7, "Database Error", $"Failed to load tracks: {errorMessage}", "OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to refresh track list");
|
|
||||||
UpdateStatus($"Error loading tracks: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle track selection changes
|
|
||||||
/// </summary>
|
|
||||||
private void OnTrackSelectionChanged(ListViewItemEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.Item >= 0 && args.Item < _tracks.Count)
|
|
||||||
{
|
|
||||||
var selectedTrack = _tracks[args.Item];
|
|
||||||
UpdateStatus($"Selected: {selectedTrack.TrackName} by {selectedTrack.Artist} - Press Enter for full details");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show help dialog with keyboard shortcuts
|
|
||||||
/// </summary>
|
|
||||||
private void ShowHelp()
|
|
||||||
{
|
|
||||||
var helpText =
|
|
||||||
"DeepDrft CLI - Interactive Mode Help\n\n" +
|
|
||||||
"KEYBOARD SHORTCUTS (also shown in legend at bottom):\n" +
|
|
||||||
"Ctrl+A - Add new track\n" +
|
|
||||||
"Ctrl+E - Edit selected track\n" +
|
|
||||||
"Delete - Delete selected track\n" +
|
|
||||||
"F5 - Refresh track list\n" +
|
|
||||||
"Enter - Show track details\n" +
|
|
||||||
"Ctrl+L - Clear status\n" +
|
|
||||||
"F1 - Show this help\n" +
|
|
||||||
"Ctrl+Q - Quit application\n\n" +
|
|
||||||
"NAVIGATION:\n" +
|
|
||||||
"↑/↓ - Navigate track list\n" +
|
|
||||||
"Tab - Switch between controls\n" +
|
|
||||||
"Space - Select/activate control\n\n" +
|
|
||||||
"USER INTERFACE:\n" +
|
|
||||||
"• Track list shows: ID │ Name │ Artist │ Album │ Genre\n" +
|
|
||||||
"• Status bar provides real-time feedback\n" +
|
|
||||||
"• Legend bar shows common shortcuts\n" +
|
|
||||||
"• Menu bar accessible via Alt or mouse\n\n" +
|
|
||||||
"HIGH CONTRAST COLOR SCHEME:\n" +
|
|
||||||
"Bright Red - Required fields (*)\n" +
|
|
||||||
"Bright Yellow - Selected/focused items\n" +
|
|
||||||
"Blue Background - Selection highlight\n" +
|
|
||||||
"Bright Cyan - Status messages & info\n" +
|
|
||||||
"Bright Green - Legend shortcuts\n" +
|
|
||||||
"Bright White - Normal text & borders";
|
|
||||||
|
|
||||||
MessageBox.Query(70, 22, "Help - Interactive Mode Guide", helpText, "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show about dialog
|
|
||||||
/// </summary>
|
|
||||||
private void ShowAbout()
|
|
||||||
{
|
|
||||||
var aboutText =
|
|
||||||
"DeepDrft CLI - Interactive Mode\n\n" +
|
|
||||||
"Version: 1.0.0\n" +
|
|
||||||
"Built with Terminal.Gui\n\n" +
|
|
||||||
"Features:\n" +
|
|
||||||
"• Interactive track management\n" +
|
|
||||||
"• Dual database architecture\n" +
|
|
||||||
"• WAV file processing\n" +
|
|
||||||
"• Color-coded interface\n" +
|
|
||||||
"• Keyboard shortcuts\n\n" +
|
|
||||||
"© 2025 DeepDrft Project";
|
|
||||||
|
|
||||||
MessageBox.Query(50, 12, "About DeepDrft CLI", aboutText, "OK");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the status display
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateStatus(string message)
|
|
||||||
{
|
|
||||||
if (_statusView != null)
|
|
||||||
{
|
|
||||||
_statusView.Text = $"Status: {message}";
|
|
||||||
_statusView.SetNeedsDisplay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clear the status display
|
|
||||||
/// </summary>
|
|
||||||
private void ClearStatus()
|
|
||||||
{
|
|
||||||
UpdateStatus("Ready");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
namespace DeepDrftCli.Utils;
|
|
||||||
|
|
||||||
internal static class CliUtils
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Truncates a string to fit within the specified column width,
|
|
||||||
/// appending "..." when the string is longer.
|
|
||||||
/// </summary>
|
|
||||||
internal static string TruncateString(string input, int maxLength)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(input))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"CliSettings": {
|
|
||||||
"ConnectionString": "Host=localhost;Port=5433;Database=postgres;Username=postgres;Password=your-password-here",
|
|
||||||
"VaultPath": "C:/Development/DeepDrftHome/Database/Vaults"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -76,18 +76,6 @@ Global
|
|||||||
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x64.Build.0 = Release|Any CPU
|
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.ActiveCfg = Release|Any CPU
|
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.Build.0 = Release|Any CPU
|
{C79AFD08-02C0-45D2-A98A-FCDDFBEAE155}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{EEB3A665-B8AD-4C00-A41E-B9D8AFE1BBA8}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{47E99024-491B-47A6-BAF8-9E5814366DB2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
|||||||
Reference in New Issue
Block a user