Update files
Signed-off-by: Lev Rusanov <30170278+JDM170@users.noreply.github.com>
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="KeyListener.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
339
KeyListener.cs
Normal file
339
KeyListener.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EpisodeRenamer
|
||||
{
|
||||
/// <summary>
|
||||
/// Класс для обработки горячих клавиш с использованием Task
|
||||
/// </summary>
|
||||
public class HotkeyListener : IDisposable
|
||||
{
|
||||
#region Windows API Imports
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr GetConsoleWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
#endregion
|
||||
|
||||
#region Key Codes
|
||||
private static class VirtualKeyCodes
|
||||
{
|
||||
public const int VK_CONTROL = 0x11;
|
||||
public const int VK_ALT = 0x12;
|
||||
public const int VK_SHIFT = 0x10;
|
||||
public const int VK_R = 0x52;
|
||||
public const int VK_ESCAPE = 0x1B;
|
||||
// Добавьте другие коды клавиш по необходимости
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Events and Delegates
|
||||
/// <summary>
|
||||
/// Делегат для события нажатия комбинации клавиш
|
||||
/// </summary>
|
||||
public delegate void HotkeyEventHandler(string combinationName);
|
||||
|
||||
/// <summary>
|
||||
/// Событие, возникающее при нажатии зарегистрированной комбинации клавиш
|
||||
/// </summary>
|
||||
public event HotkeyEventHandler OnHotkeyPressed;
|
||||
|
||||
/// <summary>
|
||||
/// Событие, возникающее при ошибке в работе слушателя
|
||||
/// </summary>
|
||||
public event Action<Exception> OnError;
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
private readonly List<KeyCombination> _keyCombinations;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private Task _listenerTask;
|
||||
private bool _isDisposed;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _isRunning;
|
||||
#endregion
|
||||
|
||||
#region Key Combination Class
|
||||
/// <summary>
|
||||
/// Класс для описания комбинации клавиш
|
||||
/// </summary>
|
||||
private class KeyCombination
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int[] KeyCodes { get; set; }
|
||||
public int CooldownMs { get; set; } = 300;
|
||||
public DateTime LastTriggered { get; set; } = DateTime.MinValue;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Создает новый экземпляр слушателя горячих клавиш
|
||||
/// </summary>
|
||||
public HotkeyListener()
|
||||
{
|
||||
_keyCombinations = new List<KeyCombination>();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Добавляем комбинацию по умолчанию
|
||||
RegisterCombination("ControlAltR",
|
||||
new[] { VirtualKeyCodes.VK_CONTROL, VirtualKeyCodes.VK_ALT, VirtualKeyCodes.VK_R });
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Регистрирует новую комбинацию клавиш для отслеживания
|
||||
/// </summary>
|
||||
/// <param name="name">Имя комбинации</param>
|
||||
/// <param name="keyCodes">Коды клавиш (все должны быть нажаты одновременно)</param>
|
||||
/// <param name="cooldownMs">Время задержки между срабатываниями (мс)</param>
|
||||
public void RegisterCombination(string name, int[] keyCodes, int cooldownMs = 300)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("Имя комбинации не может быть пустым", nameof(name));
|
||||
|
||||
if (keyCodes == null || keyCodes.Length == 0)
|
||||
throw new ArgumentException("Массив кодов клавиш не может быть пустым", nameof(keyCodes));
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
var existing = _keyCombinations.Find(k => k.Name == name);
|
||||
if (existing != null)
|
||||
{
|
||||
_keyCombinations.Remove(existing);
|
||||
}
|
||||
|
||||
_keyCombinations.Add(new KeyCombination
|
||||
{
|
||||
Name = name,
|
||||
KeyCodes = keyCodes,
|
||||
CooldownMs = cooldownMs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет комбинацию клавиш из отслеживания
|
||||
/// </summary>
|
||||
/// <param name="name">Имя комбинации</param>
|
||||
public void UnregisterCombination(string name)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
var combination = _keyCombinations.Find(k => k.Name == name);
|
||||
if (combination != null)
|
||||
{
|
||||
_keyCombinations.Remove(combination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает прослушивание горячих клавиш
|
||||
/// </summary>
|
||||
public async Task StartListeningAsync()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Переводим консоль на передний план для лучшего захвата клавиш
|
||||
BringConsoleToFront();
|
||||
|
||||
_listenerTask = Task.Run(() => ListenForHotkeys(_cancellationTokenSource.Token),
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Останавливает прослушивание горячих клавиш
|
||||
/// </summary>
|
||||
public async Task StopListeningAsync()
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
if (_listenerTask != null && !_listenerTask.IsCompleted)
|
||||
{
|
||||
await _listenerTask.ContinueWith(t =>
|
||||
{
|
||||
// Игнорируем исключения отмены задачи
|
||||
if (t.IsFaulted) OnError?.Invoke(t.Exception);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ожидаемое исключение при отмене
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает статус работы слушателя
|
||||
/// </summary>
|
||||
public bool IsListening => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Получает список зарегистрированных комбинаций
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> RegisteredCombinations
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _keyCombinations.ConvertAll(k => k.Name).AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private async Task ListenForHotkeys(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(10, cancellationToken); // Небольшая задержка для снижения нагрузки
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
foreach (var combination in _keyCombinations)
|
||||
{
|
||||
if (IsCombinationPressed(combination.KeyCodes))
|
||||
{
|
||||
// Проверяем cooldown
|
||||
if ((DateTime.Now - combination.LastTriggered).TotalMilliseconds >= combination.CooldownMs)
|
||||
{
|
||||
combination.LastTriggered = DateTime.Now;
|
||||
TriggerHotkeyEvent(combination.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ожидаемое исключение при отмене
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCombinationPressed(int[] keyCodes)
|
||||
{
|
||||
foreach (var keyCode in keyCodes)
|
||||
{
|
||||
if (!IsKeyPressed(keyCode))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsKeyPressed(int keyCode)
|
||||
{
|
||||
return (GetAsyncKeyState(keyCode) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
private void TriggerHotkeyEvent(string combinationName)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnHotkeyPressed?.Invoke(combinationName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError?.Invoke(new InvalidOperationException(
|
||||
$"Ошибка при обработке события для комбинации {combinationName}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
private void BringConsoleToFront()
|
||||
{
|
||||
try
|
||||
{
|
||||
var consoleHandle = GetConsoleWindow();
|
||||
SetForegroundWindow(consoleHandle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Не критичная ошибка, просто логируем
|
||||
OnError?.Invoke(new InvalidOperationException("Не удалось перевести консоль на передний план", ex));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
StopListeningAsync().GetAwaiter().GetResult();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
~HotkeyListener()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
68
Program.cs
68
Program.cs
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EpisodeRenamer
|
||||
{
|
||||
class EpisodeRenamer
|
||||
{
|
||||
private class PatternConfig
|
||||
@@ -39,7 +42,42 @@ class EpisodeRenamer
|
||||
}
|
||||
}
|
||||
|
||||
static void Main()
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
// Создаем экземпляр слушателя
|
||||
using (var hotkeyListener = new HotkeyListener())
|
||||
{
|
||||
// Подписываемся на события
|
||||
hotkeyListener.OnHotkeyPressed += (combination) =>
|
||||
{
|
||||
switch (combination)
|
||||
{
|
||||
case "ControlAltR":
|
||||
LoadConfiguration(true);
|
||||
break;
|
||||
case "ControlC":
|
||||
Console.WriteLine("\nВыход из программы.");
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
hotkeyListener.OnError += (ex) => Console.WriteLine($"Ошибка: {ex.Message}");
|
||||
hotkeyListener.RegisterCombination("ControlC", new[] { 0x11, 0x43 });
|
||||
|
||||
// Запускаем прослушивание
|
||||
await hotkeyListener.StartListeningAsync();
|
||||
|
||||
// Основной цикл программы
|
||||
await RunMainProgramLoop();
|
||||
|
||||
// Останавливаем слушатель (автоматически вызывается в Dispose)
|
||||
await hotkeyListener.StopListeningAsync();
|
||||
}
|
||||
}
|
||||
|
||||
static async Task RunMainProgramLoop()
|
||||
{
|
||||
Console.Title = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyTitleAttribute>().Title;
|
||||
|
||||
@@ -55,13 +93,14 @@ class EpisodeRenamer
|
||||
return;
|
||||
}
|
||||
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Console.WriteLine("\nВыход из программы.");
|
||||
Environment.Exit(0);
|
||||
};
|
||||
//Console.CancelKeyPress += (sender, e) =>
|
||||
//{
|
||||
// Console.WriteLine("\nВыход из программы.");
|
||||
// Environment.Exit(0);
|
||||
//};
|
||||
|
||||
Console.WriteLine("Чтобы оставить текущую директорию нажмите 'Enter'");
|
||||
Console.WriteLine("Чтобы перезагрузить конфигурацию нажмите 'Ctrl + Alt + R'");
|
||||
Console.WriteLine("Чтобы выйти нажмите 'Ctrl + C'");
|
||||
|
||||
while (true)
|
||||
@@ -69,7 +108,8 @@ class EpisodeRenamer
|
||||
try
|
||||
{
|
||||
Console.Write("\nВведите путь до папки с эпизодами: ");
|
||||
string input = Console.ReadLine().Trim() ?? string.Empty;
|
||||
string input = Console.ReadLine() ?? string.Empty;
|
||||
input = input.Trim();
|
||||
|
||||
string folder = string.IsNullOrEmpty(input)
|
||||
? Directory.GetCurrentDirectory()
|
||||
@@ -91,8 +131,15 @@ class EpisodeRenamer
|
||||
}
|
||||
}
|
||||
|
||||
private static void LoadConfiguration()
|
||||
private static void LoadConfiguration(bool reloadConfiguration = false)
|
||||
{
|
||||
if (reloadConfiguration)
|
||||
{
|
||||
extensions.Clear();
|
||||
patterns.Clear();
|
||||
Console.WriteLine("\nЗапрошена перезагрузка конфигурации.");
|
||||
}
|
||||
|
||||
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
@@ -183,3 +230,4 @@ class EpisodeRenamer
|
||||
return index.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user