From 69c8fcfe3c168c0025dfa3c80655369d897a08e7 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 7 Sep 2025 12:58:43 -0400 Subject: [PATCH] Front End Rework & Deployment Cleanup - Restructured references to service projects instead of ASP.NET Core web projects - Terminal.Gui front end for easy track management from the shell --- DeepDrftCli/CLAUDE.md | 12 + DeepDrftCli/DeepDrftCli.csproj | 21 +- DeepDrftCli/Models/CliSettings.cs | 8 + DeepDrftCli/Program.cs | 48 +- DeepDrftCli/Services/CliService.cs | 11 +- DeepDrftCli/Services/GuiService.cs | 913 +++++++++++++++++++++++++++++ DeepDrftCli/appsettings.json | 15 - dch5-publish-cli.sh | 112 ++++ 8 files changed, 1098 insertions(+), 42 deletions(-) create mode 100644 DeepDrftCli/Models/CliSettings.cs create mode 100644 DeepDrftCli/Services/GuiService.cs delete mode 100644 DeepDrftCli/appsettings.json create mode 100644 dch5-publish-cli.sh diff --git a/DeepDrftCli/CLAUDE.md b/DeepDrftCli/CLAUDE.md index 297aa22..57ff18c 100644 --- a/DeepDrftCli/CLAUDE.md +++ b/DeepDrftCli/CLAUDE.md @@ -60,6 +60,18 @@ public async Task AddTrackFromWavAsync( ### Available Commands +#### GUI Mode (Interactive Terminal Interface) +```bash +DeepDrftCli gui +``` +Launches the interactive Terminal.Gui interface with: +- **DeepDrft brand color theme** (Magenta/Purple/Pink) +- **Color-coded track list** with navigation +- **Persistent hotkey legend** showing shortcuts +- **Interactive add track dialog** with file browser +- **Real-time status updates** and feedback +- **Full keyboard shortcuts** for all operations + #### Add Track ```bash DeepDrftCli add [album] [genre] [release-date] diff --git a/DeepDrftCli/DeepDrftCli.csproj b/DeepDrftCli/DeepDrftCli.csproj index 56dd022..d429aee 100644 --- a/DeepDrftCli/DeepDrftCli.csproj +++ b/DeepDrftCli/DeepDrftCli.csproj @@ -8,24 +8,39 @@ + + + - - + + - + + PreserveNewest + + PreserveNewest + + + true + + + + + + diff --git a/DeepDrftCli/Models/CliSettings.cs b/DeepDrftCli/Models/CliSettings.cs new file mode 100644 index 0000000..f400a9f --- /dev/null +++ b/DeepDrftCli/Models/CliSettings.cs @@ -0,0 +1,8 @@ +namespace DeepDrftCli.Models +{ + public class CliSettings + { + public string ConnectionString { get; set; } = string.Empty; + public string VaultPath { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/DeepDrftCli/Program.cs b/DeepDrftCli/Program.cs index 94ee9f6..31634ae 100644 --- a/DeepDrftCli/Program.cs +++ b/DeepDrftCli/Program.cs @@ -3,43 +3,34 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration; using Microsoft.EntityFrameworkCore; -using DeepDrftWeb.Data; -using DeepDrftWeb.Data.Repositories; -using DeepDrftContent.FileDatabase.Services; -using DeepDrftContent.Processors; -using DeepDrftContent.Services; +using DeepDrftWeb.Services.Data; +using DeepDrftWeb.Services.Repositories; +using DeepDrftContent.Services.FileDatabase.Services; +using DeepDrftContent.Services.Processors; using DeepDrftCli.Services; +using DeepDrftCli.Models; var builder = Host.CreateApplicationBuilder(args); -// Add configuration -var appDirectory = AppContext.BaseDirectory; -var configPath = Path.Combine(appDirectory, "appsettings.json"); -builder.Configuration.AddJsonFile(configPath, optional: false, reloadOnChange: true); +// Load configuration from environment/config.json +builder.Configuration.AddJsonFile($"{AppDomain.CurrentDomain.BaseDirectory}environment/connections.json", optional: false, reloadOnChange: true); +var cliSettings = builder.Configuration.GetSection(nameof(CliSettings)).Get(); +if (cliSettings is null) { throw new Exception("CLI settings are not configured"); } // Add logging builder.Services.AddLogging(configure => configure.AddConsole()); // Add database context -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); -if (string.IsNullOrEmpty(connectionString)) - throw new InvalidOperationException("DefaultConnection not found in configuration"); - builder.Services.AddDbContext(options => - options.UseSqlite(connectionString)); + options.UseSqlite(cliSettings.ConnectionString)); // Add FileDatabase builder.Services.AddSingleton(provider => { var logger = provider.GetRequiredService>(); - var configuration = provider.GetRequiredService(); try { - var vaultPath = configuration["FileDatabaseSettings:VaultPath"]; - if (string.IsNullOrEmpty(vaultPath)) - throw new InvalidOperationException("FileDatabaseSettings:VaultPath not found in configuration"); - - var fileDatabase = FileDatabase.FromAsync(vaultPath).GetAwaiter().GetResult(); + var fileDatabase = FileDatabase.FromAsync(cliSettings.VaultPath).GetAwaiter().GetResult(); if (fileDatabase == null) { logger.LogError("Failed to initialize FileDatabase"); @@ -60,10 +51,21 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Build and run var app = builder.Build(); -// Get the CLI service and run -var cliService = app.Services.GetRequiredService(); -await cliService.RunAsync(args); \ No newline at end of file +// 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(); + await guiService.RunAsync(); +} +else +{ + // Run traditional CLI mode + var cliService = app.Services.GetRequiredService(); + await cliService.RunAsync(args); +} \ No newline at end of file diff --git a/DeepDrftCli/Services/CliService.cs b/DeepDrftCli/Services/CliService.cs index b8d6502..5c37f6b 100644 --- a/DeepDrftCli/Services/CliService.cs +++ b/DeepDrftCli/Services/CliService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; -using DeepDrftWeb.Data.Repositories; +using Microsoft.Extensions.DependencyInjection; +using DeepDrftWeb.Services.Repositories; using DeepDrftContent.Services; using DeepDrftModels.Entities; using NetBlocks.Models; @@ -50,6 +51,10 @@ public class CliService 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": @@ -244,6 +249,10 @@ public class CliService { 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 [album] [genre] [release-date]"); Console.WriteLine(" - Adds a WAV file to both SQL and FileDatabase"); diff --git a/DeepDrftCli/Services/GuiService.cs b/DeepDrftCli/Services/GuiService.cs new file mode 100644 index 0000000..908bfe9 --- /dev/null +++ b/DeepDrftCli/Services/GuiService.cs @@ -0,0 +1,913 @@ +using Microsoft.Extensions.Logging; +using Terminal.Gui; +using DeepDrftWeb.Services.Repositories; +using DeepDrftContent.Services; +using DeepDrftModels.Entities; +using NetBlocks.Models; + +namespace DeepDrftCli.Services; + +/// +/// Terminal.Gui based interactive interface for DeepDrft CLI operations +/// +public class GuiService +{ + private readonly ILogger _logger; + private readonly TrackRepository _trackRepository; + private readonly DeepDrftWeb.Services.TrackService _webTrackService; + private readonly DeepDrftContent.Services.TrackService _contentTrackService; + + // GUI Components + private Window? _mainWindow; + private MenuBar? _menuBar; + private ListView? _trackListView; + private TextView? _statusView; + private FrameView? _legendFrame; + private List _tracks = new(); + + public GuiService( + ILogger logger, + TrackRepository trackRepository, + DeepDrftWeb.Services.TrackService webTrackService, + DeepDrftContent.Services.TrackService contentTrackService) + { + _logger = logger; + _trackRepository = trackRepository; + _webTrackService = webTrackService; + _contentTrackService = contentTrackService; + } + + /// + /// Initialize and run the GUI application + /// + 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(); + } + } + + /// + /// Setup the main application window with all components + /// + 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"); + } + + /// + /// Setup the menu bar with color-coded options + /// + 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); + } + + /// + /// Setup the track list view with color coding + /// + 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; + } + + /// + /// Setup the status view at the bottom + /// + 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) + }; + } + + /// + /// Setup the hotkey legend panel at the bottom + /// + 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); + } + + /// + /// Create the formatted legend text with shortcuts + /// + private string CreateLegendText() + { + return "Ctrl+A: Add │ Ctrl+E: Edit │ Del: Delete │ F5: Refresh │ Enter: Details │ F1: Help │ Ctrl+Q: Quit"; + } + + /// + /// Setup keyboard shortcuts + /// + 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; + } + }; + } + + /// + /// Show the Add Track dialog with form validation + /// + 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); + } + + /// + /// Show the Edit Track dialog for the selected track + /// + 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); + } + + /// + /// Show the Delete Track confirmation dialog for the selected track + /// + 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); + }); + } + } + + /// + /// Delete the specified track from the database + /// + 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"); + }); + } + } + + /// + /// Validate input and add track to database + /// + private async Task 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; + } + } + + /// + /// Validate input and update existing track in database + /// + private async Task 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; + } + } + + /// + /// Show detailed information about the selected track + /// + 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"); + } + + /// + /// Refresh the track list from database + /// + 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} │ {TruncateString(t.TrackName, 25),25} │ {TruncateString(t.Artist, 20),20} │ {TruncateString(t.Album ?? "", 15),15} │ {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}"); + } + } + + /// + /// Handle track selection changes + /// + 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"); + } + } + + /// + /// Show help dialog with keyboard shortcuts + /// + 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"); + } + + /// + /// Show about dialog + /// + 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"); + } + + /// + /// Update the status display + /// + private void UpdateStatus(string message) + { + if (_statusView != null) + { + _statusView.Text = $"Status: {message}"; + _statusView.SetNeedsDisplay(); + } + } + + /// + /// Clear the status display + /// + private void ClearStatus() + { + UpdateStatus("Ready"); + } + + /// + /// Truncate string to fit display width + /// + private string TruncateString(string input, int maxLength) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + + return input.Length <= maxLength ? input : input.Substring(0, maxLength - 3) + "..."; + } +} \ No newline at end of file diff --git a/DeepDrftCli/appsettings.json b/DeepDrftCli/appsettings.json deleted file mode 100644 index cab3d07..0000000 --- a/DeepDrftCli/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "System": "Warning" - } - }, - "ConnectionStrings": { - "DefaultConnection": "Data Source=F:\\Development\\DeepDrftHome\\Database\\deepdrft.db" - }, - "FileDatabaseSettings": { - "VaultPath": "F:\\Development\\DeepDrftHome\\Database\\Vaults" - } -} \ No newline at end of file diff --git a/dch5-publish-cli.sh b/dch5-publish-cli.sh new file mode 100644 index 0000000..066d818 --- /dev/null +++ b/dch5-publish-cli.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +echo "🚀 Starting CLI deployment process..." + +# start SSH agent and add key +echo "🔑 Starting SSH agent and adding deployment key..." +eval $(ssh-agent -s) +ssh-add /c/.ssh/deepdrft_ed25519 +echo "✅ SSH agent configured" + +CLI_PROJ="DeepDrftCli" +CLI_APP="deepdrft-cli.tar.gz" + +# Publish CLI with framework-dependent single file +echo "🔨 Publishing CLI project for linux-x64..." +dotnet publish $CLI_PROJ -c Release -f net9.0 -o $CLI_PROJ/publish -r linux-x64 \ + --self-contained false \ + -p:PublishSingleFile=true \ + -p:Platform="Any CPU" \ + --verbosity normal + +if [ $? -eq 0 ]; then + echo "✅ CLI project published successfully" +else + echo "❌ Failed to publish CLI project" + exit 1 +fi + +# Eliminate local environment from package +echo "🧹 Removing local environment from package..." +rm -rf $CLI_PROJ/publish/environment +echo "✅ Local environment removed" + +# Compress published files +echo "📦 Compressing published files..." +tar -czf $CLI_APP -C $CLI_PROJ/publish . +echo "✅ Package created: $CLI_APP" + +# Deploy +REMOTE="deepdrft@dch5.snailbird.net" +CLI_APPROOT="/deepdrft/cli" + +echo "🌐 Deploying to remote server: $REMOTE" +echo "📁 Target directory: $CLI_APPROOT" + +echo "🗑️ Cleaning existing deployment..." +ssh $REMOTE "rm -rf $CLI_APPROOT/bin/*" +echo "✅ Remote directory cleaned" + +echo "📤 Uploading package to remote server..." +scp $CLI_APP $REMOTE:$CLI_APPROOT/$CLI_APP +if [ $? -eq 0 ]; then + echo "✅ Package uploaded successfully" +else + echo "❌ Failed to upload package" + exit 1 +fi + +echo "📦 Extracting and setting up CLI on remote server..." +ssh $REMOTE "tar -xzf $CLI_APPROOT/$CLI_APP -C $CLI_APPROOT/bin && \ + chmod +x $CLI_APPROOT/bin/DeepDrftCli && \ + rm $CLI_APPROOT/$CLI_APP" +if [ $? -eq 0 ]; then + echo "✅ CLI extracted and configured on remote server" +else + echo "❌ Failed to extract CLI on remote server" + exit 1 +fi + +# Apply Local Environment (if exists) +echo "🔧 Checking for local environment configuration..." +if ssh $REMOTE "[ -d $CLI_APPROOT/environment ]"; then + echo "📋 Local environment found, applying configuration..." + + # Ensure environment directory exists in the binary location + ssh $REMOTE "mkdir -p $CLI_APPROOT/bin/environment" + + # Copy environment files with better error handling + if ssh $REMOTE "cp $CLI_APPROOT/environment/* $CLI_APPROOT/bin/environment/ 2>/dev/null"; then + echo "✅ Local environment configuration applied successfully" + else + echo "⚠️ Warning: Some environment files may not have been copied" + fi +else + echo "ℹ️ No local environment configuration found - skipping" +fi + +echo "🔗 Setting up user-accessible command symlink..." +# Create user-accessible symlink without sudo +ssh $REMOTE "mkdir -p ~/bin && ln -sf $CLI_APPROOT/bin/DeepDrftCli ~/bin/deepdrft" +if [ $? -eq 0 ]; then + echo "✅ Symlink created successfully" +else + echo "❌ Failed to create symlink" + exit 1 +fi + +echo "🛣️ Ensuring ~/bin is in PATH..." +# Ensure ~/bin is in PATH (add to .bashrc if not present) +ssh $REMOTE "grep -q '~/bin' ~/.bashrc || echo 'export PATH=\"\$HOME/bin:\$PATH\"' >> ~/.bashrc" +echo "✅ PATH configuration updated" + +echo "🧹 Cleaning up local files..." +# Clean up +rm -rf ./$CLI_PROJ/publish +rm -f ./$CLI_APP +ssh-agent -k +echo "✅ Local cleanup completed" + +echo "" +echo "🎉 CLI deployment complete!" +echo "📝 Note: Run 'source ~/.bashrc' or start a new shell session to activate the deepdrft command in PATH" \ No newline at end of file