Files
clipforge/ClipForge/MainWindow.xaml.cs

602 lines
23 KiB
C#

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Win32;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Windows.System;
using WinRT.Interop;
namespace ClipForge
{
public sealed partial class MainWindow : Window
{
private GlobalHotkeyService _hotkeyService;
private ScreenCaptureService _captureService;
private ClipLibraryService _clipLibrary;
private SettingsService _settingsService;
private ThumbnailService _thumbnailService;
private bool _isRecordingHotkey;
private delegate IntPtr WinProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private WinProc _newWndProc;
private IntPtr _oldWndProc;
[DllImport("user32.dll")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, WinProc newProc);
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr LoadImage(IntPtr hInst, string lpszName,
uint uType, int cxDesired, int cyDesired, uint fuLoad);
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, uint msg,
IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vk);
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int VK_MENU = 0x12; // Alt
private const int VK_LWIN = 0x5B;
private const int GWLP_WNDPROC = -4;
private const uint WM_HOTKEY = 0x0312;
private const int GWL_EXSTYLE = -20;
private const int WS_EX_NOACTIVATE = 0x08000000;
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const int WS_EX_TOPMOST = 0x00000008;
public MainWindow()
{
this.InitializeComponent();
var hwnd = WindowNative.GetWindowHandle(this);
// Set window icon
var iconPath = System.IO.Path.Combine(
AppContext.BaseDirectory, "clipforge.ico");
if (System.IO.File.Exists(iconPath))
{
var hIcon = LoadImage(IntPtr.Zero, iconPath, 1, 32, 32, 0x0010);
SendMessage(hwnd, 0x0080, new IntPtr(1), hIcon);
SendMessage(hwnd, 0x0080, new IntPtr(0), hIcon);
}
_newWndProc = WndProc;
_oldWndProc = SetWindowLongPtr(hwnd, GWLP_WNDPROC, _newWndProc);
_captureService = new ScreenCaptureService();
_clipLibrary = new ClipLibraryService();
_clipLibrary.Initialize();
_settingsService = new SettingsService();
_settingsService.Load();
_thumbnailService = new ThumbnailService();
_hotkeyService = new GlobalHotkeyService();
_hotkeyService.ClipRequested += OnClipRequested;
var mod = _settingsService.Settings.HotkeyModifiers;
var vk = _settingsService.Settings.HotkeyVirtualKey;
if (mod == 0 && vk == 0) { mod = 1; vk = 0x78; }
_hotkeyService.Initialize(hwnd, (uint)mod, (uint)vk);
ClipGrid.ItemsSource = _clipLibrary.Clips;
UpdateClipCount();
_clipLibrary.Clips.CollectionChanged += (s, e) => UpdateClipCount();
this.AppWindow.Closing += OnWindowClosing;
this.Activated += OnWindowActivated;
}
private void OnWindowClosing(
Microsoft.UI.Windowing.AppWindow sender,
Microsoft.UI.Windowing.AppWindowClosingEventArgs args)
{
args.Cancel = true;
this.AppWindow.Hide();
var app = Microsoft.UI.Xaml.Application.Current as App;
app?.TrayIcon?.SetStatus("Running in background");
}
private void UpdateClipCount()
{
ClipCountText.Text = $"{_clipLibrary.Clips.Count} clips";
}
private async void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
this.Activated -= OnWindowActivated;
await System.Threading.Tasks.Task.Delay(500);
StartCaptureAsync();
ApplySettings();
GenerateThumbnailsAsync();
}
private async void StartCaptureAsync()
{
var hwnd = WindowNative.GetWindowHandle(this);
var success = await _captureService.StartCaptureAsync(hwnd);
if (success)
await ShowToastAsync("⬡ Recording started");
else
await ShowToastAsync("❌ Failed to start recording");
}
private async void GenerateThumbnailsAsync()
{
await _thumbnailService.GenerateAllThumbnailsAsync(_clipLibrary.Clips);
}
private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WM_HOTKEY)
_hotkeyService.ProcessHotkey((int)wParam);
return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam);
}
private async void OnClipRequested()
{
try
{
await ShowToastAsync("⬡ Saving clip...");
var savedPath = await _captureService.SaveClipAsync(
_settingsService.Settings.ClipLengthSeconds);
if (savedPath != null)
{
await ShowToastAsync("✅ Clip saved!");
var newClip = _clipLibrary.Clips.FirstOrDefault(
c => c.Path == savedPath);
if (newClip != null)
await _thumbnailService.GenerateThumbnailAsync(newClip);
}
else
{
await ShowToastAsync("❌ Failed to save clip");
}
}
catch (Exception ex)
{
await ShowToastAsync($"❌ {ex.Message}");
}
}
private void ApplySettings()
{
var s = _settingsService.Settings;
ClipLengthSlider.Value = s.ClipLengthSeconds;
QualitySlider.Value = s.VideoQuality;
ClipLengthLabel.Text = $"{s.ClipLengthSeconds} seconds";
QualityLabel.Text = $"{s.VideoQuality}%";
FramerateCombo.SelectedIndex = s.Framerate == 30 ? 0 : 1;
StartupToggle.IsOn = IsStartupEnabled();
if (HotkeyRecorderButton != null)
HotkeyRecorderButton.Content = HotkeyHelper.ToDisplayString((uint)s.HotkeyModifiers, (uint)s.HotkeyVirtualKey);
}
// --- STARTUP WITH WINDOWS ---
private const string StartupKey =
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
private const string StartupValueName = "ClipForge";
private bool IsStartupEnabled()
{
using var key = Registry.CurrentUser.OpenSubKey(StartupKey);
return key?.GetValue(StartupValueName) != null;
}
private void SetStartup(bool enable)
{
using var key = Registry.CurrentUser.OpenSubKey(StartupKey, true);
if (enable)
{
var exePath = System.Diagnostics.Process
.GetCurrentProcess().MainModule?.FileName ?? "";
key?.SetValue(StartupValueName, $"\"{exePath}\"");
}
else
{
key?.DeleteValue(StartupValueName, false);
}
}
private void StartupToggle_Toggled(object sender, RoutedEventArgs e)
{
SetStartup(StartupToggle.IsOn);
}
// --- SETTINGS ---
private void ClipLengthSlider_ValueChanged(object sender,
Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
if (ClipLengthLabel == null) return;
int val = (int)e.NewValue;
ClipLengthLabel.Text = $"{val} seconds";
_settingsService.Settings.ClipLengthSeconds = val;
}
private void QualitySlider_ValueChanged(object sender,
Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
if (QualityLabel == null) return;
int val = (int)e.NewValue;
QualityLabel.Text = $"{val}%";
_settingsService.Settings.VideoQuality = val;
}
private void FramerateCombo_SelectionChanged(object sender,
Microsoft.UI.Xaml.Controls.SelectionChangedEventArgs e)
{
if (_settingsService == null) return;
_settingsService.Settings.Framerate =
FramerateCombo.SelectedIndex == 0 ? 30 : 60;
}
private void SaveSettings_Click(object sender, RoutedEventArgs e)
{
_settingsService.Save();
_ = ShowToastAsync("✅ Settings saved!");
}
// --- CUSTOM HOTKEY ---
private void HotkeyRecorderButton_Click(object sender, RoutedEventArgs e)
{
if (_isRecordingHotkey) return;
_isRecordingHotkey = true;
HotkeyRecorderButton.Content = "Press any key... (Esc to cancel)";
HotkeyRecorderButton.Focus(FocusState.Programmatic);
HotkeyRecorderButton.KeyDown += OnHotkeyCaptureKeyDown;
}
private void OnHotkeyCaptureKeyDown(object sender, KeyRoutedEventArgs e)
{
if (!_isRecordingHotkey) return;
e.Handled = true;
try
{
var key = e.Key;
// Escape cancels without changing the hotkey
if (key == VirtualKey.Escape)
{
StopRecordingHotkey(restoreDisplay: true);
return;
}
if (HotkeyHelper.IsModifierKey(key))
return; // wait for a non-modifier key
if (key == VirtualKey.None)
return;
uint mod = 0;
if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) mod |= HotkeyHelper.MOD_CONTROL;
if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) mod |= HotkeyHelper.MOD_ALT;
if ((GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0) mod |= HotkeyHelper.MOD_SHIFT;
if ((GetAsyncKeyState(VK_LWIN) & 0x8000) != 0) mod |= HotkeyHelper.MOD_WIN;
uint vk = (uint)key;
_settingsService.Settings.HotkeyModifiers = (int)mod;
_settingsService.Settings.HotkeyVirtualKey = (int)vk;
var display = HotkeyHelper.ToDisplayString(mod, vk);
StopRecordingHotkey(restoreDisplay: false);
HotkeyRecorderButton.Content = display;
// Defer registration so the key is released first; otherwise the new hotkey can fire
// immediately and re-enter (e.g. OnClipRequested) and crash.
_ = DelayedHotkeyRegister(mod, vk, display);
}
catch (Exception ex)
{
StopRecordingHotkey(restoreDisplay: true);
_ = ShowToastAsync("⚠ " + ex.Message);
}
}
private void StopRecordingHotkey(bool restoreDisplay)
{
HotkeyRecorderButton.KeyDown -= OnHotkeyCaptureKeyDown;
_isRecordingHotkey = false;
if (restoreDisplay)
{
var s = _settingsService.Settings;
HotkeyRecorderButton.Content = HotkeyHelper.ToDisplayString((uint)s.HotkeyModifiers, (uint)s.HotkeyVirtualKey);
}
}
private async System.Threading.Tasks.Task DelayedHotkeyRegister(uint mod, uint vk, string display)
{
await System.Threading.Tasks.Task.Delay(300);
App.MainQueue?.TryEnqueue(() =>
{
try
{
var ok = _hotkeyService.UpdateHotkey(mod, vk);
HotkeyRecorderButton.Content = ok ? display : display + " (in use?)";
}
catch (Exception ex)
{
HotkeyRecorderButton.Content = display;
_ = ShowToastAsync("⚠ " + ex.Message);
}
});
}
// --- NAV ---
private void NavClips_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Visible;
SettingsPage.Visibility = Visibility.Collapsed;
NavClips.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonSelectedStyle"];
NavSettings.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonStyle"];
}
private void NavSettings_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Collapsed;
SettingsPage.Visibility = Visibility.Visible;
NavSettings.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonSelectedStyle"];
NavClips.Style = (Microsoft.UI.Xaml.Style)Application.Current.Resources["ClipForgeNavButtonStyle"];
}
private void RecordButton_Click(object sender, RoutedEventArgs e)
{
if (_captureService.IsCapturing)
{
_captureService.StopCapture();
RecordLabel.Text = "START";
RecordDot.Fill = new Microsoft.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(255, 100, 100, 100));
}
else
{
StartCaptureAsync();
RecordLabel.Text = "CAPTURING";
RecordDot.Fill = new Microsoft.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(255, 255, 71, 87));
}
}
private void OpenFolder_Click(object sender, RoutedEventArgs e)
{
System.Diagnostics.Process.Start("explorer.exe",
_clipLibrary.ClipsDirectory);
}
private void SearchBox_TextChanged(AutoSuggestBox sender,
AutoSuggestBoxTextChangedEventArgs args)
{
var query = sender.Text.ToLower();
if (string.IsNullOrWhiteSpace(query))
ClipGrid.ItemsSource = _clipLibrary.Clips;
else
ClipGrid.ItemsSource = _clipLibrary.Clips
.Where(c => c.Title.ToLower().Contains(query))
.ToList();
}
// --- CLIP ACTIONS ---
private void TrimClip_Click(object sender, RoutedEventArgs e)
{
var button = sender as Microsoft.UI.Xaml.Controls.Button;
var clipPath = button?.Tag as string;
if (clipPath == null) return;
var trimmer = new TrimmerWindow(clipPath);
trimmer.Activate();
}
private async void DeleteClip_Click(object sender, RoutedEventArgs e)
{
var button = sender as Microsoft.UI.Xaml.Controls.Button;
var clipPath = button?.Tag as string;
if (clipPath == null) return;
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = "Delete Clip",
Content = "Are you sure? This cannot be undone.",
PrimaryButtonText = "Delete",
CloseButtonText = "Cancel",
DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Close,
XamlRoot = this.Content.XamlRoot
};
var result = await dialog.ShowAsync();
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary)
{
var clip = _clipLibrary.Clips.FirstOrDefault(c => c.Path == clipPath);
if (clip != null)
{
_thumbnailService.DeleteThumbnail(clip);
_clipLibrary.DeleteClip(clip);
}
}
}
private async void RenameClip_Click(object sender, RoutedEventArgs e)
{
var button = sender as Microsoft.UI.Xaml.Controls.Button;
var clipPath = button?.Tag as string;
if (clipPath == null) return;
var clip = _clipLibrary.Clips.FirstOrDefault(c => c.Path == clipPath);
if (clip == null) return;
var input = new Microsoft.UI.Xaml.Controls.TextBox
{
Text = clip.Title,
PlaceholderText = "Enter new name"
};
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = "Rename Clip",
Content = input,
PrimaryButtonText = "Rename",
CloseButtonText = "Cancel",
DefaultButton = Microsoft.UI.Xaml.Controls.ContentDialogButton.Primary,
XamlRoot = this.Content.XamlRoot
};
var result = await dialog.ShowAsync();
if (result == Microsoft.UI.Xaml.Controls.ContentDialogResult.Primary)
{
var newName = input.Text.Trim();
if (string.IsNullOrEmpty(newName)) return;
var dir = System.IO.Path.GetDirectoryName(clipPath)!;
var newPath = System.IO.Path.Combine(dir, newName + ".mp4");
var newThumbPath = System.IO.Path.Combine(dir, newName + ".thumb.png");
try
{
System.IO.File.Move(clipPath, newPath);
if (System.IO.File.Exists(clip.ThumbnailPath))
System.IO.File.Move(clip.ThumbnailPath, newThumbPath);
clip.Path = newPath;
clip.Title = newName;
clip.ThumbnailPath = newThumbPath;
ClipGrid.ItemsSource = null;
ClipGrid.ItemsSource = _clipLibrary.Clips;
}
catch (Exception ex)
{
await ShowToastAsync($"❌ {ex.Message}");
}
}
}
// --- TOAST ---
private async System.Threading.Tasks.Task ShowToastAsync(string message)
{
var overlayWindow = new Microsoft.UI.Xaml.Window();
var textBlock = new Microsoft.UI.Xaml.Controls.TextBlock
{
Text = message,
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(255, 232, 255, 71)),
FontSize = 14,
FontWeight = Microsoft.UI.Text.FontWeights.Bold,
VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center
};
var border = new Microsoft.UI.Xaml.Controls.Border
{
Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(
Windows.UI.Color.FromArgb(230, 18, 18, 24)),
CornerRadius = new Microsoft.UI.Xaml.CornerRadius(10),
Padding = new Microsoft.UI.Xaml.Thickness(20, 12, 20, 12),
Child = textBlock,
RenderTransform = new Microsoft.UI.Xaml.Media.TranslateTransform { X = 40 },
Opacity = 0
};
overlayWindow.Content = border;
var hwnd = WindowNative.GetWindowHandle(overlayWindow);
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
int toastWidth = 220;
int toastHeight = 48;
int margin = 20;
var displayArea = Microsoft.UI.Windowing.DisplayArea.Primary;
int x = displayArea.WorkArea.Width - toastWidth - margin;
int y = displayArea.WorkArea.Y + margin + 40;
appWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, toastWidth, toastHeight));
SetWindowLong(hwnd, GWL_EXSTYLE,
GetWindowLong(hwnd, GWL_EXSTYLE)
| WS_EX_NOACTIVATE
| WS_EX_TOOLWINDOW
| WS_EX_TOPMOST);
var overlayPresenter = Microsoft.UI.Windowing.OverlappedPresenter.Create();
overlayPresenter.IsAlwaysOnTop = true;
overlayPresenter.IsResizable = false;
overlayPresenter.IsMaximizable = false;
overlayPresenter.IsMinimizable = false;
overlayPresenter.SetBorderAndTitleBar(false, false);
appWindow.SetPresenter(overlayPresenter);
var titleBar = appWindow.TitleBar;
titleBar.ExtendsContentIntoTitleBar = true;
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
overlayWindow.Activate();
var slideIn = new DoubleAnimation
{
From = 40,
To = 0,
Duration = new Duration(TimeSpan.FromMilliseconds(250)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var fadeIn = new DoubleAnimation
{
From = 0,
To = 1,
Duration = new Duration(TimeSpan.FromMilliseconds(200)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
};
var storyboardIn = new Storyboard();
Storyboard.SetTarget(slideIn, border);
Storyboard.SetTargetProperty(slideIn, "(UIElement.RenderTransform).(TranslateTransform.X)");
Storyboard.SetTarget(fadeIn, border);
Storyboard.SetTargetProperty(fadeIn, "Opacity");
storyboardIn.Children.Add(slideIn);
storyboardIn.Children.Add(fadeIn);
storyboardIn.Begin();
await System.Threading.Tasks.Task.Delay(1500);
var slideOut = new DoubleAnimation
{
From = 0,
To = 40,
Duration = new Duration(TimeSpan.FromMilliseconds(200)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
};
var fadeOut = new DoubleAnimation
{
From = 1,
To = 0,
Duration = new Duration(TimeSpan.FromMilliseconds(200)),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn }
};
var storyboardOut = new Storyboard();
Storyboard.SetTarget(slideOut, border);
Storyboard.SetTargetProperty(slideOut, "(UIElement.RenderTransform).(TranslateTransform.X)");
Storyboard.SetTarget(fadeOut, border);
Storyboard.SetTargetProperty(fadeOut, "Opacity");
storyboardOut.Children.Add(slideOut);
storyboardOut.Children.Add(fadeOut);
var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
storyboardOut.Completed += (s, e) => tcs.SetResult(true);
storyboardOut.Begin();
await tcs.Task;
overlayWindow.Close();
}
}
}