mirror of
https://github.com/skoelle/marcer-gamedvd-launcher.git
synced 2026-09-17 18:50:25 +00:00
cleanup
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
// Manages loading, saving and querying favorite ZIP paths.
|
// Manages loading, saving and querying favorite ZIP paths.
|
||||||
public class FavoritesService
|
public class FavoritesService(string filePath)
|
||||||
{
|
{
|
||||||
// Virtual folder name displayed at the root when favorites exist.
|
// Virtual folder name displayed at the root when favorites exist.
|
||||||
// Not configurable — fixed UI element.
|
// Not configurable — fixed UI element.
|
||||||
@@ -14,15 +14,10 @@ namespace MarcerGameDvdLauncher
|
|||||||
// Not configurable — fixed persistence file.
|
// Not configurable — fixed persistence file.
|
||||||
public const string DefaultFileName = "favorites.txt";
|
public const string DefaultFileName = "favorites.txt";
|
||||||
|
|
||||||
private readonly string _filePath;
|
private readonly string _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
||||||
// Use a SortedSet so favorites are kept in sorted order in memory.
|
// Use a SortedSet so favorites are kept in sorted order in memory.
|
||||||
private SortedSet<string> _favorites = new(StringComparer.OrdinalIgnoreCase);
|
private SortedSet<string> _favorites = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public FavoritesService(string filePath)
|
|
||||||
{
|
|
||||||
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load favorites from disk (no-op if file missing)
|
// Load favorites from disk (no-op if file missing)
|
||||||
public void Load()
|
public void Load()
|
||||||
{
|
{
|
||||||
@@ -31,7 +26,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
var lines = File.ReadAllLines(_filePath, System.Text.Encoding.UTF8);
|
var lines = File.ReadAllLines(_filePath, System.Text.Encoding.UTF8);
|
||||||
foreach (var l in lines)
|
foreach (var l in lines)
|
||||||
{
|
{
|
||||||
var t = l?.Trim();
|
var t = l.Trim();
|
||||||
if (string.IsNullOrEmpty(t)) continue;
|
if (string.IsNullOrEmpty(t)) continue;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -71,14 +66,13 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path));
|
||||||
var full = Path.GetFullPath(path);
|
var full = Path.GetFullPath(path);
|
||||||
bool added;
|
bool added;
|
||||||
if (_favorites.Contains(full))
|
if (!_favorites.Add(full))
|
||||||
{
|
{
|
||||||
_favorites.Remove(full);
|
_favorites.Remove(full);
|
||||||
added = false;
|
added = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_favorites.Add(full);
|
|
||||||
added = true;
|
added = true;
|
||||||
}
|
}
|
||||||
Save();
|
Save();
|
||||||
|
|||||||
@@ -44,15 +44,8 @@ namespace MarcerGameDvdLauncher
|
|||||||
throw new ArgumentException("ZIP archive path must not be empty.", nameof(zipFilePath));
|
throw new ArgumentException("ZIP archive path must not be empty.", nameof(zipFilePath));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string args = _argsTemplate;
|
var args = _argsTemplate;
|
||||||
if (!string.IsNullOrWhiteSpace(_cfgPath))
|
args = args.Replace("{cfg}", !string.IsNullOrWhiteSpace(_cfgPath) ? _cfgPath : Path.Combine(Directory.GetCurrentDirectory(),DEFAULT_CONFIG_FILE));
|
||||||
{
|
|
||||||
args = args.Replace("{cfg}", _cfgPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
args = args.Replace("{cfg}", Path.Combine(Directory.GetCurrentDirectory(),DEFAULT_CONFIG_FILE));
|
|
||||||
}
|
|
||||||
args = args.Replace("{zip}", zipFilePath);
|
args = args.Replace("{zip}", zipFilePath);
|
||||||
|
|
||||||
var psi = new System.Diagnostics.ProcessStartInfo
|
var psi = new System.Diagnostics.ProcessStartInfo
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
// Licensed under the MIT License. See LICENSE file in project root for details.
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -13,42 +8,25 @@ namespace MarcerGameDvdLauncher
|
|||||||
/// Extracted from AppHost so that LauncherApp stays focused on lifecycle management.
|
/// Extracted from AppHost so that LauncherApp stays focused on lifecycle management.
|
||||||
/// No functional change - all key-handling behaviour is preserved exactly.
|
/// No functional change - all key-handling behaviour is preserved exactly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class InputController
|
internal class InputController(
|
||||||
|
OverlayDirectoryBrowser directoryBrowser,
|
||||||
|
MenuRenderer menuRenderer,
|
||||||
|
NavigationController navigationController,
|
||||||
|
HatariLauncher hatariLauncher,
|
||||||
|
FavoritesService favoritesService,
|
||||||
|
UIErrorService errorService)
|
||||||
{
|
{
|
||||||
private readonly OverlayDirectoryBrowser _directoryBrowser;
|
|
||||||
private readonly MenuRenderer _menuRenderer;
|
|
||||||
private readonly NavigationController _navigationController;
|
|
||||||
private readonly HatariLauncher _hatariLauncher;
|
|
||||||
private readonly FavoritesService _favoritesService;
|
|
||||||
private readonly UIErrorService _errorService;
|
|
||||||
|
|
||||||
private List<GameEntry> _gameEntries = new();
|
private List<GameEntry> _gameEntries = new();
|
||||||
|
|
||||||
public IReadOnlyList<GameEntry> GameEntries => _gameEntries;
|
|
||||||
|
|
||||||
public InputController(OverlayDirectoryBrowser directoryBrowser, MenuRenderer menuRenderer,
|
|
||||||
NavigationController navigationController, HatariLauncher hatariLauncher,
|
|
||||||
FavoritesService favoritesService, UIErrorService errorService)
|
|
||||||
{
|
|
||||||
_directoryBrowser = directoryBrowser;
|
|
||||||
_menuRenderer = menuRenderer;
|
|
||||||
_navigationController = navigationController;
|
|
||||||
_hatariLauncher = hatariLauncher;
|
|
||||||
_favoritesService = favoritesService;
|
|
||||||
_errorService = errorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsFavorite(GameEntry e)
|
private bool IsFavorite(GameEntry e)
|
||||||
{
|
{
|
||||||
return e.Kind == EntryKind.Zip
|
return e.Kind == EntryKind.Zip && favoritesService.IsFavorite(e.InPatch ? e.PatchPath : e.RootPath);
|
||||||
? _favoritesService.IsFavorite(e.InPatch ? e.PatchPath : e.RootPath)
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMenu(int availableLines)
|
private void DrawMenu(int availableLines)
|
||||||
{
|
{
|
||||||
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset,
|
menuRenderer.DrawMenu(_gameEntries, navigationController.ScrollOffset,
|
||||||
_navigationController.SelectedIndex, availableLines, IsFavorite);
|
navigationController.SelectedIndex, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,7 +34,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RefreshView(int availableLines)
|
public void RefreshView(int availableLines)
|
||||||
{
|
{
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +46,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// If we are at the virtual Favorites folder, produce the flat list from the favorites service
|
// If we are at the virtual Favorites folder, produce the flat list from the favorites service
|
||||||
if (string.Equals(_navigationController.CurrentRelativePath, FavoritesService.FavoritesRootName, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(navigationController.CurrentRelativePath, FavoritesService.FavoritesRootName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var favs = _favoritesService.GetAll();
|
var favs = favoritesService.GetAll();
|
||||||
_gameEntries = new List<GameEntry>();
|
_gameEntries = new List<GameEntry>();
|
||||||
foreach (var p in favs)
|
foreach (var p in favs)
|
||||||
{
|
{
|
||||||
@@ -85,15 +63,15 @@ namespace MarcerGameDvdLauncher
|
|||||||
IsVirtual = false
|
IsVirtual = false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_navigationController.SetEntriesCount(_gameEntries.Count);
|
navigationController.SetEntriesCount(_gameEntries.Count);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise use the overlay directory browser for normal folders
|
// Otherwise use the overlay directory browser for normal folders
|
||||||
_gameEntries = _directoryBrowser.GetEntries(_navigationController.CurrentRelativePath);
|
_gameEntries = directoryBrowser.GetEntries(navigationController.CurrentRelativePath);
|
||||||
|
|
||||||
// If we are at the root and there are favorites, prepend a virtual Favorites folder
|
// If we are at the root and there are favorites, prepend a virtual Favorites folder
|
||||||
if (string.IsNullOrEmpty(_navigationController.CurrentRelativePath) && _favoritesService.HasFavorites())
|
if (string.IsNullOrEmpty(navigationController.CurrentRelativePath) && favoritesService.HasFavorites())
|
||||||
{
|
{
|
||||||
var virtualEntry = new GameEntry
|
var virtualEntry = new GameEntry
|
||||||
{
|
{
|
||||||
@@ -108,14 +86,14 @@ namespace MarcerGameDvdLauncher
|
|||||||
_gameEntries.Insert(0, virtualEntry);
|
_gameEntries.Insert(0, virtualEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigationController.SetEntriesCount(_gameEntries.Count);
|
navigationController.SetEntriesCount(_gameEntries.Count);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Show the error to the user and continue with an empty list
|
// Show the error to the user and continue with an empty list
|
||||||
_errorService.ShowError(ex.Message);
|
errorService.ShowError(ex.Message);
|
||||||
_gameEntries = new List<GameEntry>();
|
_gameEntries = new List<GameEntry>();
|
||||||
_navigationController.SetEntriesCount(0);
|
navigationController.SetEntriesCount(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +104,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
if (key.KeyChar == '?')
|
if (key.KeyChar == '?')
|
||||||
{
|
{
|
||||||
_menuRenderer.ShowHelpBox(availableLines);
|
menuRenderer.ShowHelpBox(availableLines);
|
||||||
Console.ReadKey(intercept: true);
|
Console.ReadKey(intercept: true);
|
||||||
_menuRenderer.InvalidateCache();
|
menuRenderer.InvalidateCache();
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
return false;
|
return false;
|
||||||
@@ -138,16 +116,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
case ConsoleKey.UpArrow:
|
case ConsoleKey.UpArrow:
|
||||||
{
|
{
|
||||||
int previousSelectedIndex = _navigationController.SelectedIndex;
|
int previousSelectedIndex = navigationController.SelectedIndex;
|
||||||
bool didScroll = _navigationController.MoveUp(_gameEntries, availableLines);
|
bool didScroll = navigationController.MoveUp(_gameEntries, availableLines);
|
||||||
if (didScroll)
|
if (didScroll)
|
||||||
{
|
{
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - _navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, _navigationController.SelectedIndex, _navigationController.SelectedIndex - _navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, navigationController.SelectedIndex, navigationController.SelectedIndex - navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
// flush input to avoid key repeat
|
// flush input to avoid key repeat
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
@@ -155,16 +133,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
break;
|
break;
|
||||||
case ConsoleKey.DownArrow:
|
case ConsoleKey.DownArrow:
|
||||||
{
|
{
|
||||||
int previousSelectedIndex = _navigationController.SelectedIndex;
|
int previousSelectedIndex = navigationController.SelectedIndex;
|
||||||
bool didScroll = _navigationController.MoveDown(_gameEntries, availableLines);
|
bool didScroll = navigationController.MoveDown(_gameEntries, availableLines);
|
||||||
if (didScroll)
|
if (didScroll)
|
||||||
{
|
{
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - _navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, previousSelectedIndex, previousSelectedIndex - navigationController.ScrollOffset, false, availableLines, IsFavorite);
|
||||||
_menuRenderer.RedrawEntry(_gameEntries, _navigationController.SelectedIndex, _navigationController.SelectedIndex - _navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
menuRenderer.RedrawEntry(_gameEntries, navigationController.SelectedIndex, navigationController.SelectedIndex - navigationController.ScrollOffset, true, availableLines, IsFavorite);
|
||||||
}
|
}
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
@@ -172,26 +150,26 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Enter:
|
case ConsoleKey.Enter:
|
||||||
case ConsoleKey.RightArrow:
|
case ConsoleKey.RightArrow:
|
||||||
{
|
{
|
||||||
var oldRelativePath = _navigationController.CurrentRelativePath;
|
var oldRelativePath = navigationController.CurrentRelativePath;
|
||||||
var isDirectory = _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
var isDirectory = _gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
||||||
_navigationController.HandleEnter(_gameEntries);
|
navigationController.HandleEnter(_gameEntries);
|
||||||
if (isDirectory && oldRelativePath != _navigationController.CurrentRelativePath)
|
if (isDirectory && oldRelativePath != navigationController.CurrentRelativePath)
|
||||||
{
|
{
|
||||||
ReloadGameEntries();
|
ReloadGameEntries();
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
}
|
}
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
// Only start a ZIP if NOT switching to a directory
|
// Only start a ZIP if NOT switching to a directory
|
||||||
if (!isDirectory && _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
if (!isDirectory && _gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
string zipToLaunch = _gameEntries[_navigationController.SelectedIndex].InPatch ? _gameEntries[_navigationController.SelectedIndex].PatchPath : _gameEntries[_navigationController.SelectedIndex].RootPath;
|
string zipToLaunch = _gameEntries[navigationController.SelectedIndex].InPatch ? _gameEntries[navigationController.SelectedIndex].PatchPath : _gameEntries[navigationController.SelectedIndex].RootPath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_hatariLauncher.Launch(zipToLaunch);
|
hatariLauncher.Launch(zipToLaunch);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorService.ShowError(ex.Message);
|
errorService.ShowError(ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// flush input to avoid leftover key events after an enter/navigation
|
// flush input to avoid leftover key events after an enter/navigation
|
||||||
@@ -201,23 +179,23 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Backspace:
|
case ConsoleKey.Backspace:
|
||||||
case ConsoleKey.LeftArrow:
|
case ConsoleKey.LeftArrow:
|
||||||
{
|
{
|
||||||
_navigationController.GoUpDirectory();
|
navigationController.GoUpDirectory();
|
||||||
ReloadGameEntries();
|
ReloadGameEntries();
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
navigationController.UpdateScrollOffset(_gameEntries.Count, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.PageDown:
|
case ConsoleKey.PageDown:
|
||||||
{
|
{
|
||||||
_navigationController.PageDown(_gameEntries, availableLines);
|
navigationController.PageDown(_gameEntries, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.PageUp:
|
case ConsoleKey.PageUp:
|
||||||
{
|
{
|
||||||
_navigationController.PageUp(_gameEntries, availableLines);
|
navigationController.PageUp(_gameEntries, availableLines);
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
}
|
}
|
||||||
@@ -226,17 +204,17 @@ namespace MarcerGameDvdLauncher
|
|||||||
case ConsoleKey.Oem8:
|
case ConsoleKey.Oem8:
|
||||||
{
|
{
|
||||||
// Toggle favorite for selected ZIP (handles numpad * and some layouts)
|
// Toggle favorite for selected ZIP (handles numpad * and some layouts)
|
||||||
if (_gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
if (_gameEntries.Count > 0 && _gameEntries[navigationController.SelectedIndex].Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
var ge = _gameEntries[_navigationController.SelectedIndex];
|
var ge = _gameEntries[navigationController.SelectedIndex];
|
||||||
string path = ge.InPatch ? ge.PatchPath : ge.RootPath;
|
string path = ge.InPatch ? ge.PatchPath : ge.RootPath;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_favoritesService.Toggle(path);
|
favoritesService.Toggle(path);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_errorService.ShowError("Failed to toggle favorite: " + ex.Message);
|
errorService.ShowError("Failed to toggle favorite: " + ex.Message);
|
||||||
}
|
}
|
||||||
// Redraw the whole menu so the '*' marker updates immediately
|
// Redraw the whole menu so the '*' marker updates immediately
|
||||||
DrawMenu(availableLines);
|
DrawMenu(availableLines);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
{
|
{
|
||||||
public class MenuRenderer
|
public class MenuRenderer(AppColorConfig? colors = null)
|
||||||
{
|
{
|
||||||
// Simple line-based double buffer to avoid full Clear() flicker.
|
// Simple line-based double buffer to avoid full Clear() flicker.
|
||||||
// cachedBuffer holds the last rendered text and colors for each visible row.
|
// cachedBuffer holds the last rendered text and colors for each visible row.
|
||||||
@@ -12,12 +12,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
private int _cachedWidth = -1;
|
private int _cachedWidth = -1;
|
||||||
|
|
||||||
// Color configuration (injected; defaults to built-in scheme if null)
|
// Color configuration (injected; defaults to built-in scheme if null)
|
||||||
private readonly AppColorConfig _colors;
|
private readonly AppColorConfig _colors = colors ?? new AppColorConfig();
|
||||||
|
|
||||||
public MenuRenderer(AppColorConfig? colors = null)
|
|
||||||
{
|
|
||||||
_colors = colors ?? new AppColorConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
// availableLines is provided per-draw so the renderer adapts to console resizes
|
// availableLines is provided per-draw so the renderer adapts to console resizes
|
||||||
public void DrawMenu(List<GameEntry> entries, int scrollOffset, int selectedIndex, int availableLines, Func<GameEntry, bool>? isFavorite = null)
|
public void DrawMenu(List<GameEntry> entries, int scrollOffset, int selectedIndex, int availableLines, Func<GameEntry, bool>? isFavorite = null)
|
||||||
@@ -140,53 +135,40 @@ namespace MarcerGameDvdLauncher
|
|||||||
|
|
||||||
// Renders a centered, bordered help box with key bindings inside the
|
// Renders a centered, bordered help box with key bindings inside the
|
||||||
// available console area. The caller is responsible for waiting on a
|
// available console area. The caller is responsible for waiting on a
|
||||||
// key and redrawing the menu afterwards.
|
// key and redrawing the menu afterward.
|
||||||
public void ShowHelpBox(int availableLines)
|
public void ShowHelpBox(int availableLines)
|
||||||
{
|
{
|
||||||
try
|
var width = Console.WindowWidth;
|
||||||
|
var helpLines = GetHelpLines();
|
||||||
|
var boxHeight = Math.Min(helpLines.Length + 2, Math.Max(3, availableLines));
|
||||||
|
var boxWidth = Math.Max(1, width);
|
||||||
|
var topRow = Math.Max(0, (availableLines - boxHeight) / 2);
|
||||||
|
|
||||||
|
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
|
||||||
|
var topBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
||||||
|
Console.SetCursorPosition(0, topRow);
|
||||||
|
Console.Write(topBorder);
|
||||||
|
|
||||||
|
for (var i = 0; i < boxHeight - 2; i++)
|
||||||
{
|
{
|
||||||
int width = Console.WindowWidth;
|
var row = topRow + 1 + i;
|
||||||
string[] helpLines = GetHelpLines();
|
var content = i < helpLines.Length
|
||||||
int boxHeight = Math.Min(helpLines.Length + 2, Math.Max(3, availableLines));
|
? PadToWidth(helpLines[i], boxWidth - 2)
|
||||||
int boxWidth = Math.Max(1, width);
|
: new string(' ', Math.Max(0, boxWidth - 2));
|
||||||
int topRow = Math.Max(0, (availableLines - boxHeight) / 2);
|
Console.SetCursorPosition(0, row);
|
||||||
|
Console.Write("|" + content + "|");
|
||||||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
|
||||||
|
|
||||||
string topBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
|
||||||
Console.SetCursorPosition(0, topRow);
|
|
||||||
Console.Write(topBorder);
|
|
||||||
|
|
||||||
for (int i = 0; i < boxHeight - 2; i++)
|
|
||||||
{
|
|
||||||
int row = topRow + 1 + i;
|
|
||||||
string content;
|
|
||||||
if (i < helpLines.Length)
|
|
||||||
{
|
|
||||||
content = PadToWidth(helpLines[i], boxWidth - 2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
content = new string(' ', Math.Max(0, boxWidth - 2));
|
|
||||||
}
|
|
||||||
Console.SetCursorPosition(0, row);
|
|
||||||
Console.Write("|" + content + "|");
|
|
||||||
}
|
|
||||||
|
|
||||||
int bottomRow = topRow + boxHeight - 1;
|
|
||||||
if (bottomRow < Console.WindowHeight)
|
|
||||||
{
|
|
||||||
string bottomBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
|
||||||
Console.SetCursorPosition(0, bottomRow);
|
|
||||||
Console.Write(bottomBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
|
var bottomRow = topRow + boxHeight - 1;
|
||||||
|
if (bottomRow < Console.WindowHeight)
|
||||||
{
|
{
|
||||||
|
var bottomBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
||||||
|
Console.SetCursorPosition(0, bottomRow);
|
||||||
|
Console.Write(bottomBorder);
|
||||||
}
|
}
|
||||||
|
Console.ResetColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] GetHelpLines()
|
private static string[] GetHelpLines()
|
||||||
@@ -235,7 +217,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
if (width <= 0) return string.Empty;
|
if (width <= 0) return string.Empty;
|
||||||
|
|
||||||
string label = GetLabel(e, isFavorite);
|
var label = GetLabel(e, isFavorite);
|
||||||
|
|
||||||
// If the console width is smaller than the label, truncate the label
|
// If the console width is smaller than the label, truncate the label
|
||||||
if (width <= label.Length)
|
if (width <= label.Length)
|
||||||
@@ -243,7 +225,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
return label.Substring(0, width);
|
return label.Substring(0, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
int maxNameLen = width - label.Length; // space left for name
|
var maxNameLen = width - label.Length; // space left for name
|
||||||
string displayName;
|
string displayName;
|
||||||
if (e.Name.Length <= maxNameLen)
|
if (e.Name.Length <= maxNameLen)
|
||||||
{
|
{
|
||||||
@@ -257,7 +239,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
displayName = e.Name.Substring(0, Math.Max(0, maxNameLen));
|
displayName = e.Name.Substring(0, Math.Max(0, maxNameLen));
|
||||||
}
|
}
|
||||||
|
|
||||||
int padding = Math.Max(0, width - label.Length - displayName.Length);
|
var padding = Math.Max(0, width - label.Length - displayName.Length);
|
||||||
var result = label + displayName + new string(' ', padding);
|
var result = label + displayName + new string(' ', padding);
|
||||||
// Ensure exact width (defensive): truncate or pad if needed
|
// Ensure exact width (defensive): truncate or pad if needed
|
||||||
if (result.Length > width) return result.Substring(0, width);
|
if (result.Length > width) return result.Substring(0, width);
|
||||||
@@ -273,7 +255,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
{
|
{
|
||||||
// Layer label (7 chars)
|
// Layer label (7 chars)
|
||||||
string layer;
|
string layer;
|
||||||
if (e.InRoot && e.InPatch) layer = "[BOTH] ";
|
if (e is {InRoot: true, InPatch: true}) layer = "[BOTH] ";
|
||||||
else if (e.InPatch) layer = "[PTCH] ";
|
else if (e.InPatch) layer = "[PTCH] ";
|
||||||
else if (e.InRoot) layer = "[ROOT] ";
|
else if (e.InRoot) layer = "[ROOT] ";
|
||||||
else layer = " ";
|
else layer = " ";
|
||||||
@@ -299,15 +281,15 @@ namespace MarcerGameDvdLauncher
|
|||||||
|
|
||||||
if (e.Kind == EntryKind.Directory)
|
if (e.Kind == EntryKind.Directory)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return _colors.FolderBoth; // Both layers
|
if (e is {InRoot: true, InPatch: true}) return _colors.FolderBoth; // Both layers
|
||||||
if (e.InPatch && !e.InRoot) return _colors.FolderPatchOnly; // Only patch
|
if (e is {InPatch: true, InRoot: false}) return _colors.FolderPatchOnly; // Only patch
|
||||||
if (e.InRoot && !e.InPatch) return _colors.FolderRootOnly; // Only root
|
if (e is {InRoot: true, InPatch: false}) return _colors.FolderRootOnly; // Only root
|
||||||
}
|
}
|
||||||
else if (e.Kind == EntryKind.Zip)
|
else if (e.Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return _colors.ZipBoth; // Both layers
|
if (e is {InRoot: true, InPatch: true}) return _colors.ZipBoth; // Both layers
|
||||||
if (e.InRoot && !e.InPatch) return _colors.ZipRootOnly; // Only root
|
if (e is { InRoot: true, InPatch: false }) return _colors.ZipRootOnly; // Only root
|
||||||
if (e.InPatch && !e.InRoot) return _colors.ZipPatchOnly; // Only patch
|
if (e is {InPatch: true, InRoot: false}) return _colors.ZipPatchOnly; // Only patch
|
||||||
}
|
}
|
||||||
return ConsoleColor.DarkGray;
|
return ConsoleColor.DarkGray;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
// Scroll fractions: when the selection cursor reaches 2/3 of the visible window
|
// Scroll fractions: when the selection cursor reaches 2/3 of the visible window
|
||||||
// height from the top, the list scrolls down; when it reaches 1/3, it scrolls up.
|
// height from the top, the list scrolls down; when it reaches 1/3, it scrolls up.
|
||||||
// Not configurable — these ratios are the established navigation behaviour.
|
// Not configurable — these ratios are the established navigation behaviour.
|
||||||
private const double BottomScrollFraction = 2.0 / 3.0;
|
private const double BOTTOM_SCROLL_FRACTION = 2.0 / 3.0;
|
||||||
private const double TopScrollFraction = 1.0 / 3.0;
|
private const double TOP_SCROLL_FRACTION = 1.0 / 3.0;
|
||||||
|
|
||||||
public int SelectedIndex { get; private set; } = 0;
|
public int SelectedIndex { get; private set; }
|
||||||
public int ScrollOffset { get; private set; } = 0;
|
public int ScrollOffset { get; private set; }
|
||||||
public string CurrentRelativePath { get; private set; } = "";
|
public string CurrentRelativePath { get; private set; } = "";
|
||||||
// Stores the last selection & scroll position for each directory
|
// Stores the last selection & scroll position for each directory
|
||||||
private readonly Dictionary<string, (int sel, int scroll)> _lastSelections = new();
|
private readonly Dictionary<string, (int sel, int scroll)> _lastSelections = new();
|
||||||
@@ -105,12 +105,12 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (availableLines < 1) availableLines = 1;
|
if (availableLines < 1) availableLines = 1;
|
||||||
if (entryCount <= availableLines) { ScrollOffset = 0; return; }
|
if (entryCount <= availableLines) { ScrollOffset = 0; return; }
|
||||||
if (SelectedIndex == 0) { ScrollOffset = 0; return; }
|
if (SelectedIndex == 0) { ScrollOffset = 0; return; }
|
||||||
int bottomScrollTrigger = ScrollOffset + (int)(availableLines * BottomScrollFraction);
|
int bottomScrollTrigger = ScrollOffset + (int)(availableLines * BOTTOM_SCROLL_FRACTION);
|
||||||
int topScrollTrigger = ScrollOffset + (int)(availableLines * TopScrollFraction);
|
int topScrollTrigger = ScrollOffset + (int)(availableLines * TOP_SCROLL_FRACTION);
|
||||||
if (SelectedIndex >= bottomScrollTrigger && (ScrollOffset + availableLines) < entryCount)
|
if (SelectedIndex >= bottomScrollTrigger && (ScrollOffset + availableLines) < entryCount)
|
||||||
ScrollOffset = SelectedIndex - (int)(availableLines * BottomScrollFraction);
|
ScrollOffset = SelectedIndex - (int)(availableLines * BOTTOM_SCROLL_FRACTION);
|
||||||
else if (SelectedIndex < topScrollTrigger && ScrollOffset > 0)
|
else if (SelectedIndex < topScrollTrigger && ScrollOffset > 0)
|
||||||
ScrollOffset = SelectedIndex - (int)(availableLines * TopScrollFraction);
|
ScrollOffset = SelectedIndex - (int)(availableLines * TOP_SCROLL_FRACTION);
|
||||||
if (ScrollOffset < 0) ScrollOffset = 0;
|
if (ScrollOffset < 0) ScrollOffset = 0;
|
||||||
if (ScrollOffset > entryCount - availableLines)
|
if (ScrollOffset > entryCount - availableLines)
|
||||||
ScrollOffset = entryCount - availableLines;
|
ScrollOffset = entryCount - availableLines;
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
|
|
||||||
public class OverlayDirectoryBrowser(string root, string patch)
|
public class OverlayDirectoryBrowser(string root, string patch)
|
||||||
{
|
{
|
||||||
public List<GameEntry> GetEntries(string currentRelativePath)
|
public List<GameEntry> GetEntries(string? currentRelativePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Normalize and protect against path traversal or absolute paths in the relative path
|
// Normalize and protect against path traversal or absolute paths in the relative path
|
||||||
string rel = currentRelativePath ?? string.Empty;
|
var rel = currentRelativePath ?? string.Empty;
|
||||||
// Remove any leading directory separators
|
// Remove any leading directory separators
|
||||||
rel = rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
rel = rel.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
// If the relative path is rooted or contains parent directory segments, reset to root
|
// If the relative path is rooted or contains parent directory segments, reset to root
|
||||||
if (Path.IsPathRooted(rel) || rel.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries).Any(p => p == ".."))
|
if (Path.IsPathRooted(rel) || rel.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries).Any(p => p == ".."))
|
||||||
{
|
{
|
||||||
rel = string.Empty;
|
rel = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
// Console window title. Not configurable — fixed application display name.
|
// Console window title. Not configurable — fixed application display name.
|
||||||
private const string DefaultTitle = "Marcer GameDVD Launcher";
|
private const string DEFAULT_TITLE = "Marcer GameDVD Launcher";
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.Title = DefaultTitle;
|
Console.Title = DEFAULT_TITLE;
|
||||||
var app = new LauncherApp();
|
var app = new LauncherApp();
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
// Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
// Licensed under the MIT License. See LICENSE file in project root for details.
|
// Licensed under the MIT License. See LICENSE file in project root for details.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MarcerGameDvdLauncher
|
namespace MarcerGameDvdLauncher
|
||||||
@@ -40,11 +39,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
// These are defined by the Windows API and do not change.
|
// These are defined by the Windows API and do not change.
|
||||||
private const int STD_INPUT_HANDLE = -10;
|
private const int STD_INPUT_HANDLE = -10;
|
||||||
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern IntPtr GetStdHandle(int nStdHandle);
|
private static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);
|
private static extern bool FlushConsoleInputBuffer(IntPtr hConsoleInput);
|
||||||
|
|
||||||
// P/Invoke to query key state (used to detect physical key release)
|
// P/Invoke to query key state (used to detect physical key release)
|
||||||
@@ -59,16 +58,16 @@ namespace MarcerGameDvdLauncher
|
|||||||
// then removed and the method returns. This is designed to be a
|
// then removed and the method returns. This is designed to be a
|
||||||
// lightweight modal for the console UI and uses the last console line
|
// lightweight modal for the console UI and uses the last console line
|
||||||
// which the application reserves for transient messages.
|
// which the application reserves for transient messages.
|
||||||
public static void ShowModalUntilReturnReleased(string message)
|
public static void ShowModalUntilReturnReleased(string? message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int lastRow = AvailableLines;
|
var lastRow = AvailableLines;
|
||||||
int width = Console.WindowWidth;
|
var width = Console.WindowWidth;
|
||||||
string text = message ?? string.Empty;
|
var text = message ?? string.Empty;
|
||||||
if (text.Length > width) text = text.Substring(0, Math.Max(0, width - 3)) + "...";
|
if (text.Length > width) text = text.Substring(0, Math.Max(0, width - 3)) + "...";
|
||||||
int padding = Math.Max(0, width - text.Length);
|
var padding = Math.Max(0, width - text.Length);
|
||||||
string line = text + new string(' ', padding);
|
var line = text + new string(' ', padding);
|
||||||
|
|
||||||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||||||
Console.ForegroundColor = ConsoleColor.White;
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
|||||||
Reference in New Issue
Block a user