Загрузка данных
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace NightPlayer
{
public partial class MediaTrimmerWindow : Window
{
// ── State ──────────────────────────────────────────────────
private readonly string _filePath;
private readonly TimeSpan _totalDuration;
private TimeSpan _startTime = TimeSpan.Zero;
private TimeSpan _endTime = TimeSpan.Zero;
// Timeline drag
private enum DragHandle { None, Start, End }
private DragHandle _dragging = DragHandle.None;
private double _timelineWidth; // updated on layout
// Suppress TextChanged feedback loop while updating from timeline
private bool _updatingBoxes;
// ── Init ───────────────────────────────────────────────────
public MediaTrimmerWindow(string filePath, TimeSpan duration)
{
InitializeComponent();
_filePath = filePath;
_totalDuration = duration;
_endTime = duration;
FileNameBlock.Text = IOPath.GetFileName(filePath);
TitleBlock.Text = $"Обрезка — {IOPath.GetFileName(filePath)}";
// Write initial time boxes without triggering sync
_updatingBoxes = true;
StartTimeBox.Text = FormatTime(TimeSpan.Zero);
EndTimeBox.Text = FormatTime(duration);
_updatingBoxes = false;
Loaded += (_, _) =>
{
_timelineWidth = TimelineCanvas.ActualWidth;
UpdateHandlesFromTimes();
UpdateDurationLabel();
};
TimelineCanvas.SizeChanged += (_, _) =>
{
_timelineWidth = TimelineCanvas.ActualWidth;
UpdateHandlesFromTimes();
};
}
// ── Timeline drag ──────────────────────────────────────────
private void Timeline_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
_timelineWidth = TimelineCanvas.ActualWidth;
var pos = e.GetPosition(TimelineCanvas).X;
double sx = Canvas.GetLeft(HandleStart);
double ex = Canvas.GetLeft(HandleEnd);
// Canvas.GetLeft returns NaN before first layout pass — default to edges
if (double.IsNaN(sx)) sx = 0;
if (double.IsNaN(ex)) ex = _timelineWidth;
// Pick whichever handle is closer
_dragging = Math.Abs(pos - sx) <= Math.Abs(pos - ex)
? DragHandle.Start : DragHandle.End;
TimelineCanvas.CaptureMouse();
e.Handled = true;
}
private void Timeline_MouseMove(object sender, MouseEventArgs e)
{
if (_dragging == DragHandle.None || e.LeftButton != MouseButtonState.Pressed) return;
if (_timelineWidth <= 0) return;
double pos = Math.Clamp(e.GetPosition(TimelineCanvas).X, 0, _timelineWidth);
double ratio = pos / _timelineWidth;
var t = TimeSpan.FromSeconds(ratio * _totalDuration.TotalSeconds);
if (_dragging == DragHandle.Start)
{
_startTime = t < _endTime - TimeSpan.FromSeconds(1) ? t : _endTime - TimeSpan.FromSeconds(1);
if (_startTime < TimeSpan.Zero) _startTime = TimeSpan.Zero;
}
else
{
_endTime = t > _startTime + TimeSpan.FromSeconds(1) ? t : _startTime + TimeSpan.FromSeconds(1);
if (_endTime > _totalDuration) _endTime = _totalDuration;
}
UpdateHandlesFromTimes();
SyncBoxesFromTimes();
UpdateDurationLabel();
}
private void Timeline_MouseUp(object sender, MouseButtonEventArgs e)
{
_dragging = DragHandle.None;
TimelineCanvas.ReleaseMouseCapture();
}
// ── Time text boxes ────────────────────────────────────────
private void TimeBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (_updatingBoxes) return;
if (StartTimeBox == null || EndTimeBox == null) return;
if (TryParseTime(StartTimeBox.Text, out var s) &&
TryParseTime(EndTimeBox.Text, out var en))
{
if (s >= TimeSpan.Zero && en <= _totalDuration && s < en)
{
_startTime = s;
_endTime = en;
UpdateHandlesFromTimes();
UpdateDurationLabel();
StartTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
EndTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
}
else
{
((TextBox)sender).Foreground = new SolidColorBrush(Color.FromRgb(0xCC, 0x00, 0x00));
}
}
else
{
((TextBox)sender).Foreground = new SolidColorBrush(Color.FromRgb(0xCC, 0x00, 0x00));
}
}
// ── UI helpers ─────────────────────────────────────────────
private void UpdateHandlesFromTimes()
{
if (_timelineWidth <= 0 || _totalDuration.TotalSeconds <= 0) return;
double trackW = _timelineWidth;
double sx = (_startTime.TotalSeconds / _totalDuration.TotalSeconds) * trackW;
double ex = (_endTime.TotalSeconds / _totalDuration.TotalSeconds) * trackW;
// Clamp handle to track
sx = Math.Clamp(sx, 0, trackW - 12);
ex = Math.Clamp(ex, 12, trackW);
Canvas.SetLeft(HandleStart, sx);
Canvas.SetLeft(HandleEnd, ex - 12); // right edge of handle at ex
// Selection highlight
Canvas.SetLeft(SelectionHighlight, sx);
SelectionHighlight.Width = Math.Max(0, ex - sx);
}
private void SyncBoxesFromTimes()
{
_updatingBoxes = true;
StartTimeBox.Text = FormatTime(_startTime);
EndTimeBox.Text = FormatTime(_endTime);
StartTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
EndTimeBox.Foreground = new SolidColorBrush(Color.FromRgb(0xDD, 0xDD, 0xDD));
_updatingBoxes = false;
}
private void UpdateDurationLabel()
{
var dur = _endTime - _startTime;
DurationLabel.Text = $"Длительность фрагмента: {FormatTime(dur)}";
}
private static string FormatTime(TimeSpan t)
=> $"{(int)t.TotalHours:D2}:{t.Minutes:D2}:{t.Seconds:D2}";
private static bool TryParseTime(string s, out TimeSpan result)
{
result = TimeSpan.Zero;
if (string.IsNullOrWhiteSpace(s)) return false;
var parts = s.Split(':');
if (parts.Length == 3 &&
int.TryParse(parts[0], out int h) &&
int.TryParse(parts[1], out int m) &&
int.TryParse(parts[2], out int sec))
{
result = new TimeSpan(h, m, sec);
return true;
}
return false;
}
// ── Trim action ────────────────────────────────────────────
private async void Trim_Click(object sender, RoutedEventArgs e)
{
// Validate selection
if (_endTime <= _startTime || (_endTime - _startTime).TotalSeconds < 1)
{
MessageBox.Show("Выделите фрагмент длиной не менее 1 секунды.",
"Обрезка", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// Ask what to do with original
var choice = MessageBox.Show(
"Перезаписать оригинальный файл?\n\n" +
"Да — перезаписать оригинал\n" +
"Нет — сохранить как новый файл",
"Обрезка",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (choice == MessageBoxResult.Cancel) return;
string outputPath;
if (choice == MessageBoxResult.Yes)
{
// Save to temp, then replace original
outputPath = IOPath.Combine(
IOPath.GetDirectoryName(_filePath)!,
$"__trim_tmp_{IOPath.GetFileName(_filePath)}");
}
else
{
// Save As dialog
var ext = IOPath.GetExtension(_filePath);
var dlg = new Microsoft.Win32.SaveFileDialog
{
Title = "Сохранить обрезанный файл",
Filter = $"Медиафайл (*{ext})|*{ext}|Все файлы (*.*)|*.*",
DefaultExt = ext,
FileName = IOPath.GetFileNameWithoutExtension(_filePath) + "_trimmed"
};
if (dlg.ShowDialog() != true) return;
outputPath = dlg.FileName;
}
BtnTrim.IsEnabled = false;
BtnCancel.IsEnabled = false;
BtnTrim.Content = "Обрезка...";
bool ok = false;
string error = "";
try
{
ok = await TrimMediaAsync(_filePath, outputPath, _startTime, _endTime,
msg => Dispatcher.Invoke(() => BtnTrim.Content = msg));
}
catch (Exception ex)
{
error = ex.Message;
}
BtnTrim.IsEnabled = true;
BtnCancel.IsEnabled = true;
BtnTrim.Content = "✂ Обрезать";
if (!ok)
{
if (File.Exists(outputPath) && outputPath.Contains("__trim_tmp_"))
File.Delete(outputPath);
MessageBox.Show(
string.IsNullOrEmpty(error)
? "Не удалось обрезать файл. Формат может не поддерживаться."
: $"Ошибка обрезки:\n{error}",
"Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
// Overwrite original if needed
if (choice == MessageBoxResult.Yes)
{
try
{
File.Delete(_filePath);
File.Move(outputPath, _filePath);
}
catch (Exception ex)
{
MessageBox.Show($"Файл обрезан, но не удалось заменить оригинал:\n{ex.Message}",
"Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
}
MessageBox.Show("Файл успешно обрезан!", "Обрезка", MessageBoxButton.OK, MessageBoxImage.Information);
DialogResult = true;
Close();
}
private static readonly HashSet<string> AudioExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".mp3", ".wav", ".flac", ".ogg", ".aiff", ".aif", ".wma", ".m4a", ".aac" };
private static readonly HashSet<string> VideoExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".m4v", ".ts" };
private static async Task<bool> TrimMediaAsync(
string input, string output, TimeSpan start, TimeSpan end,
Action<string>? progress = null)
{
string ext = IOPath.GetExtension(input).ToLowerInvariant();
// ── Audio: use NAudio (no external tools needed) ──
if (AudioExtensions.Contains(ext))
return await TrimAudioWithNAudioAsync(input, output, start, end);
// ── Video: use Xabe.FFmpeg with auto-download ──
if (VideoExtensions.Contains(ext))
return await TrimVideoWithXabeAsync(input, output, start, end, progress);
return false;
}
private static Task<bool> TrimAudioWithNAudioAsync(
string input, string output, TimeSpan start, TimeSpan end)
{
return Task.Run(() =>
{
try
{
using var reader = new NAudio.Wave.AudioFileReader(input);
reader.CurrentTime = start;
var duration = end - start;
// Always write as WAV — rename output extension if needed
string wavOutput = System.IO.Path.ChangeExtension(output, ".wav");
using (var writer = new NAudio.Wave.WaveFileWriter(wavOutput, reader.WaveFormat))
{
var buffer = new byte[reader.WaveFormat.AverageBytesPerSecond];
long bytesToRead = (long)(duration.TotalSeconds * reader.WaveFormat.AverageBytesPerSecond);
while (bytesToRead > 0)
{
int toRead = (int)Math.Min(buffer.Length, bytesToRead);
int read = reader.Read(buffer, 0, toRead);
if (read == 0) break;
writer.Write(buffer, 0, read);
bytesToRead -= read;
}
}
// If caller wanted a different extension (e.g. .mp3), rename the wav
if (!string.Equals(wavOutput, output, StringComparison.OrdinalIgnoreCase))
{
if (File.Exists(output)) File.Delete(output);
File.Move(wavOutput, output);
}
return true;
}
catch
{
return false;
}
});
}
private static async Task<bool> TrimVideoWithXabeAsync(
string input, string output, TimeSpan start, TimeSpan end,
Action<string>? progress)
{
string exeDir = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ".";
Xabe.FFmpeg.FFmpeg.SetExecutablesPath(exeDir);
bool isWindows = System.Runtime.InteropServices.RuntimeInformation
.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
string ffmpegExe = System.IO.Path.Combine(exeDir,
isWindows ? "ffmpeg.exe" : "ffmpeg");
if (!File.Exists(ffmpegExe))
{
progress?.Invoke("Загрузка FFmpeg...");
try
{
await Xabe.FFmpeg.Downloader.FFmpegDownloader.GetLatestVersion(
Xabe.FFmpeg.Downloader.FFmpegVersion.Official, exeDir);
}
catch (Exception ex)
{
throw new Exception(
$"FFmpeg не найден в папке с программой и не удалось скачать автоматически.\n" +
$"Положите ffmpeg.exe рядом с NightPlayer.exe\n\nДеталь: {ex.Message}");
}
}
if (!File.Exists(ffmpegExe))
throw new Exception(
$"ffmpeg.exe не найден по пути:\n{ffmpegExe}\n\n" +
"Скачайте ffmpeg.exe с https://ffmpeg.org/download.html и положите рядом с NightPlayer.exe");
progress?.Invoke("Обрезка...");
double durationSec = (end - start).TotalSeconds;
try
{
var ic = System.Globalization.CultureInfo.InvariantCulture;
var conversion = Xabe.FFmpeg.FFmpeg.Conversions.New()
.AddParameter(
$"-ss {start.TotalSeconds.ToString("F3", ic)} -t {durationSec.ToString("F3", ic)} " +
$"-i \"{input}\" -c copy \"{output}\"");
await conversion.Start();
return true;
}
catch (Exception ex)
{
throw new Exception($"FFmpeg завершился с ошибкой:\n{ex.Message}");
}
}
// ── Window controls ────────────────────────────────────────
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left) DragMove();
}
private void Close_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
// Alias to avoid ambiguity with System.Windows.Shapes.Path
file static class IOPath
{
public static string GetFileName(string p) => System.IO.Path.GetFileName(p);
public static string GetFileNameWithoutExtension(string p) => System.IO.Path.GetFileNameWithoutExtension(p);
public static string GetExtension(string p) => System.IO.Path.GetExtension(p);
public static string? GetDirectoryName(string p) => System.IO.Path.GetDirectoryName(p);
public static string Combine(string a, string b) => System.IO.Path.Combine(a, b);
}
}