Compare commits

11 Commits

Author SHA1 Message Date
e14598b92e Merge pull request 'hotkey-config' (#1) from hotkey-config into master
Reviewed-on: #1
2026-02-22 12:45:38 -05:00
86b4e10a83 Refactor MainWindow.xaml.cs to improve the visual feedback for active selections in navigation buttons, enhancing user experience. This update includes style adjustments to better indicate the currently selected page. 2026-02-22 12:42:09 -05:00
fa5bc81bd6 Implement visual style updates for navigation buttons in MainWindow.xaml.cs to indicate active selections. This change enhances user experience by providing clear feedback on the currently selected page. 2026-02-22 12:41:51 -05:00
fcd70b699d Refactor hotkey handling in MainWindow.xaml.cs to improve user experience by implementing a visual indicator for active hotkeys and enhancing the feedback mechanism during recording. Adjustments made to ensure better responsiveness and clarity in user interactions. 2026-02-22 12:36:49 -05:00
7f5d80ff68 Enhance hotkey recording functionality in MainWindow.xaml.cs by adding escape key support to cancel recording and improving error handling. Introduce a delayed registration mechanism to prevent immediate re-entry of hotkeys, ensuring stability during hotkey updates. 2026-02-22 12:33:55 -05:00
ae744f1077 Update MainWindow.xaml.cs to replace HotkeyRecorderText with HotkeyRecorderButton for displaying hotkey information, enhancing user interaction during hotkey recording. Adjustments made to ensure proper focus and event handling for the button. 2026-02-22 12:21:38 -05:00
9b73bdbb24 Refactor MainWindow.xaml to streamline the hotkey button definition by consolidating properties and removing unnecessary elements, improving code clarity. 2026-02-22 12:21:27 -05:00
4902802615 Update MainWindow.xaml to include a name and focus properties for the root grid, enhancing accessibility and usability. 2026-02-22 12:17:03 -05:00
6aa49c0b07 Add hotkey recording functionality in MainWindow. Users can now set custom hotkeys through a dedicated button, with real-time feedback on key presses and modifier keys. Updates to settings are reflected immediately. 2026-02-22 12:15:50 -05:00
52a2515e2d Enhance GlobalHotkeyService to allow for dynamic hotkey updates and improve user experience with customizable HotkeyModifiers and HotkeyVirtualKey settings. 2026-02-22 12:14:57 -05:00
742c1d70fe Refactor GlobalHotkeyService to support dynamic hotkey registration and update. Introduce HotkeyModifiers and HotkeyVirtualKey properties in SettingsService for user-defined hotkeys. 2026-02-22 12:14:41 -05:00
8 changed files with 487 additions and 104 deletions

View File

@@ -8,9 +8,44 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
<!-- ClipForge custom palette (less Windows, more app identity) -->
<SolidColorBrush x:Key="ClipForgeSidebarBrush" Color="#0A0A0E"/>
<SolidColorBrush x:Key="ClipForgeMainBrush" Color="#0F0F14"/>
<SolidColorBrush x:Key="ClipForgeCardBrush" Color="#16161D"/>
<SolidColorBrush x:Key="ClipForgeCardBorderBrush" Color="#252532"/>
<SolidColorBrush x:Key="ClipForgeAccentBrush" Color="#E8FF47"/>
<SolidColorBrush x:Key="ClipForgeTextPrimaryBrush" Color="#F0F0F5"/>
<SolidColorBrush x:Key="ClipForgeTextSecondaryBrush" Color="#8E8E9A"/>
<SolidColorBrush x:Key="ClipForgeNavHoverBrush" Color="#1E1E26"/>
<!-- Sidebar nav: default (unselected) -->
<Style x:Key="ClipForgeNavButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource ClipForgeTextPrimaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="12,10"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
</Style>
<!-- Sidebar nav: selected / accent -->
<Style x:Key="ClipForgeNavButtonSelectedStyle" TargetType="Button" BasedOn="{StaticResource ClipForgeNavButtonStyle}">
<Setter Property="Background" Value="{StaticResource ClipForgeAccentBrush}"/>
<Setter Property="Foreground" Value="#0A0A0E"/>
</Style>
<!-- Accent button (Save Settings, etc.) -->
<Style x:Key="ClipForgeAccentButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource ClipForgeAccentBrush}"/>
<Setter Property="Foreground" Value="#0A0A0E"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="20,10"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -18,12 +18,12 @@
<PackageCertificateThumbprint>4A55954F2A73A9D620442C7DFBFC7C95A71D8D24</PackageCertificateThumbprint>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxPackageDir>C:\Users\Blade\Desktop\Clipforge Packaged\V1\</AppxPackageDir>
<AppxPackageDir>C:\Users\Blade\Desktop\Clipforge Packaged\V0.1\</AppxPackageDir>
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Auto</AppxBundle>
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<AppInstallerUri>J:\Projects\ClipForge\ClipForge\bin\x64\Release\net8.0-windows10.0.19041.0</AppInstallerUri>
<AppInstallerUri>C:\Users\Blade\Desktop\Clipforge Packaged</AppInstallerUri>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
</PropertyGroup>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
@@ -6,39 +6,49 @@ namespace ClipForge
{
public class GlobalHotkeyService
{
// These two lines talk directly to Windows to register/unregister hotkeys
[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// This is the ID we'll use to identify our clip hotkey
private const int HOTKEY_CLIP = 1;
private const uint MOD_NOREPEAT = 0x4000; // don't fire on key repeat
// Alt key modifier
private const uint MOD_ALT = 0x0001;
// F9 key
private const uint VK_F9 = 0x78;
// This is the "event" that fires when the hotkey is pressed
public event Action? ClipRequested;
private IntPtr _hwnd;
private uint _modifiers;
private uint _vk;
public void Initialize(IntPtr hwnd)
/// <summary>Register the clip hotkey. Use settings values (e.g. HotkeyModifiers, HotkeyVirtualKey).</summary>
public void Initialize(IntPtr hwnd, uint modifiers, uint vk)
{
_hwnd = hwnd;
RegisterHotKey(_hwnd, HOTKEY_CLIP, MOD_ALT, VK_F9);
_modifiers = modifiers;
_vk = vk;
TryRegister();
}
/// <summary>Update the hotkey at runtime (e.g. after user remaps). Unregisters old and registers new.</summary>
public bool UpdateHotkey(uint modifiers, uint vk)
{
UnregisterHotKey(_hwnd, HOTKEY_CLIP);
_modifiers = modifiers;
_vk = vk;
return TryRegister();
}
private bool TryRegister()
{
uint mod = _modifiers | MOD_NOREPEAT;
return RegisterHotKey(_hwnd, HOTKEY_CLIP, mod, _vk);
}
public void ProcessHotkey(int id)
{
if (id == HOTKEY_CLIP)
{
ClipRequested?.Invoke();
}
}
public void Cleanup()

183
ClipForge/HotkeyHelper.cs Normal file
View File

@@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using Windows.System;
namespace ClipForge
{
/// <summary>Converts between hotkey modifier/vk and display strings.</summary>
public static class HotkeyHelper
{
public const uint MOD_ALT = 0x0001;
public const uint MOD_CONTROL = 0x0002;
public const uint MOD_SHIFT = 0x0004;
public const uint MOD_WIN = 0x0008;
public const uint MOD_NOREPEAT = 0x4000;
// Built in static ctor with TryAdd so duplicate VirtualKey enum values (e.g. Kana/Hangul = 0x15) don't throw.
private static readonly Dictionary<VirtualKey, string> KeyNames = new();
static HotkeyHelper()
{
TryAdd(VirtualKey.LeftButton, "LMB");
TryAdd(VirtualKey.RightButton, "RMB");
TryAdd(VirtualKey.Cancel, "Cancel");
TryAdd(VirtualKey.Back, "Backspace");
TryAdd(VirtualKey.Tab, "Tab");
TryAdd(VirtualKey.Clear, "Clear");
TryAdd(VirtualKey.Enter, "Enter");
TryAdd(VirtualKey.Shift, "Shift");
TryAdd(VirtualKey.Control, "Ctrl");
TryAdd(VirtualKey.Menu, "Alt");
TryAdd(VirtualKey.Pause, "Pause");
TryAdd(VirtualKey.CapitalLock, "Caps Lock");
TryAdd(VirtualKey.Kana, "Kana");
TryAdd(VirtualKey.Hangul, "Hangul"); // same value as Kana on some SDKs; TryAdd skips duplicate
TryAdd(VirtualKey.Junja, "Junja");
TryAdd(VirtualKey.Final, "Final");
TryAdd(VirtualKey.Hanja, "Hanja");
TryAdd(VirtualKey.Kanji, "Kanji");
TryAdd(VirtualKey.Escape, "Escape");
TryAdd(VirtualKey.Convert, "Convert");
TryAdd(VirtualKey.NonConvert, "NonConvert");
TryAdd(VirtualKey.Accept, "Accept");
TryAdd(VirtualKey.ModeChange, "ModeChange");
TryAdd(VirtualKey.Space, "Space");
TryAdd(VirtualKey.PageUp, "PgUp");
TryAdd(VirtualKey.PageDown, "PgDn");
TryAdd(VirtualKey.End, "End");
TryAdd(VirtualKey.Home, "Home");
TryAdd(VirtualKey.Left, "Left");
TryAdd(VirtualKey.Up, "Up");
TryAdd(VirtualKey.Right, "Right");
TryAdd(VirtualKey.Down, "Down");
TryAdd(VirtualKey.Select, "Select");
TryAdd(VirtualKey.Print, "Print");
TryAdd(VirtualKey.Execute, "Execute");
TryAdd(VirtualKey.Snapshot, "Print Screen");
TryAdd(VirtualKey.Insert, "Insert");
TryAdd(VirtualKey.Delete, "Delete");
TryAdd(VirtualKey.Help, "Help");
TryAdd(VirtualKey.Number0, "0");
TryAdd(VirtualKey.Number1, "1");
TryAdd(VirtualKey.Number2, "2");
TryAdd(VirtualKey.Number3, "3");
TryAdd(VirtualKey.Number4, "4");
TryAdd(VirtualKey.Number5, "5");
TryAdd(VirtualKey.Number6, "6");
TryAdd(VirtualKey.Number7, "7");
TryAdd(VirtualKey.Number8, "8");
TryAdd(VirtualKey.Number9, "9");
TryAdd(VirtualKey.A, "A");
TryAdd(VirtualKey.B, "B");
TryAdd(VirtualKey.C, "C");
TryAdd(VirtualKey.D, "D");
TryAdd(VirtualKey.E, "E");
TryAdd(VirtualKey.F, "F");
TryAdd(VirtualKey.G, "G");
TryAdd(VirtualKey.H, "H");
TryAdd(VirtualKey.I, "I");
TryAdd(VirtualKey.J, "J");
TryAdd(VirtualKey.K, "K");
TryAdd(VirtualKey.L, "L");
TryAdd(VirtualKey.M, "M");
TryAdd(VirtualKey.N, "N");
TryAdd(VirtualKey.O, "O");
TryAdd(VirtualKey.P, "P");
TryAdd(VirtualKey.Q, "Q");
TryAdd(VirtualKey.R, "R");
TryAdd(VirtualKey.S, "S");
TryAdd(VirtualKey.T, "T");
TryAdd(VirtualKey.U, "U");
TryAdd(VirtualKey.V, "V");
TryAdd(VirtualKey.W, "W");
TryAdd(VirtualKey.X, "X");
TryAdd(VirtualKey.Y, "Y");
TryAdd(VirtualKey.Z, "Z");
TryAdd(VirtualKey.LeftWindows, "Win");
TryAdd(VirtualKey.RightWindows, "Win");
TryAdd(VirtualKey.Application, "App");
TryAdd(VirtualKey.Sleep, "Sleep");
TryAdd(VirtualKey.NumberPad0, "Num 0");
TryAdd(VirtualKey.NumberPad1, "Num 1");
TryAdd(VirtualKey.NumberPad2, "Num 2");
TryAdd(VirtualKey.NumberPad3, "Num 3");
TryAdd(VirtualKey.NumberPad4, "Num 4");
TryAdd(VirtualKey.NumberPad5, "Num 5");
TryAdd(VirtualKey.NumberPad6, "Num 6");
TryAdd(VirtualKey.NumberPad7, "Num 7");
TryAdd(VirtualKey.NumberPad8, "Num 8");
TryAdd(VirtualKey.NumberPad9, "Num 9");
TryAdd(VirtualKey.Multiply, "Num *");
TryAdd(VirtualKey.Add, "Num +");
TryAdd(VirtualKey.Separator, "Num ,");
TryAdd(VirtualKey.Subtract, "Num -");
TryAdd(VirtualKey.Decimal, "Num .");
TryAdd(VirtualKey.Divide, "Num /");
TryAdd(VirtualKey.F1, "F1");
TryAdd(VirtualKey.F2, "F2");
TryAdd(VirtualKey.F3, "F3");
TryAdd(VirtualKey.F4, "F4");
TryAdd(VirtualKey.F5, "F5");
TryAdd(VirtualKey.F6, "F6");
TryAdd(VirtualKey.F7, "F7");
TryAdd(VirtualKey.F8, "F8");
TryAdd(VirtualKey.F9, "F9");
TryAdd(VirtualKey.F10, "F10");
TryAdd(VirtualKey.F11, "F11");
TryAdd(VirtualKey.F12, "F12");
TryAdd(VirtualKey.F13, "F13");
TryAdd(VirtualKey.F14, "F14");
TryAdd(VirtualKey.F15, "F15");
TryAdd(VirtualKey.F16, "F16");
TryAdd(VirtualKey.F17, "F17");
TryAdd(VirtualKey.F18, "F18");
TryAdd(VirtualKey.F19, "F19");
TryAdd(VirtualKey.F20, "F20");
TryAdd(VirtualKey.F21, "F21");
TryAdd(VirtualKey.F22, "F22");
TryAdd(VirtualKey.F23, "F23");
TryAdd(VirtualKey.F24, "F24");
TryAdd(VirtualKey.NumberKeyLock, "NumLock");
TryAdd(VirtualKey.Scroll, "Scroll Lock");
TryAdd(VirtualKey.LeftShift, "Shift");
TryAdd(VirtualKey.RightShift, "Shift");
TryAdd(VirtualKey.LeftControl, "Ctrl");
TryAdd(VirtualKey.RightControl, "Ctrl");
TryAdd(VirtualKey.LeftMenu, "Alt");
TryAdd(VirtualKey.RightMenu, "Alt");
}
private static void TryAdd(VirtualKey key, string name)
{
KeyNames.TryAdd(key, name);
}
/// <summary>Format modifier flags + virtual key as display string (e.g. "Alt + PgUp").</summary>
public static string ToDisplayString(uint modifiers, uint vk)
{
var parts = new List<string>();
if ((modifiers & MOD_WIN) != 0) parts.Add("Win");
if ((modifiers & MOD_CONTROL) != 0) parts.Add("Ctrl");
if ((modifiers & MOD_ALT) != 0) parts.Add("Alt");
if ((modifiers & MOD_SHIFT) != 0) parts.Add("Shift");
var keyName = GetKeyName((VirtualKey)vk);
if (!string.IsNullOrEmpty(keyName))
parts.Add(keyName);
return parts.Count > 0 ? string.Join(" + ", parts) : "None";
}
public static string GetKeyName(VirtualKey key)
{
return KeyNames.TryGetValue(key, out var name) ? name : key.ToString();
}
/// <summary>True if the key is typically used only as a modifier (don't use as sole key).</summary>
public static bool IsModifierKey(VirtualKey key)
{
return key == VirtualKey.LeftWindows || key == VirtualKey.RightWindows
|| key == VirtualKey.Control || key == VirtualKey.LeftControl || key == VirtualKey.RightControl
|| key == VirtualKey.Menu || key == VirtualKey.LeftMenu || key == VirtualKey.RightMenu
|| key == VirtualKey.Shift || key == VirtualKey.LeftShift || key == VirtualKey.RightShift;
}
}
}

View File

@@ -13,56 +13,71 @@
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid x:Name="RootGrid" Background="{StaticResource ClipForgeMainBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SIDEBAR -->
<Border Grid.Column="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Background="{StaticResource ClipForgeSidebarBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- App title -->
<Border Grid.Row="0" Padding="16,0">
<TextBlock Text="CLIPFORGE"
FontSize="13"
FontWeight="Bold"
CharacterSpacing="80"
Foreground="#E8FF47"
VerticalAlignment="Center"/>
<!-- Logo + wordmark -->
<Border Grid.Row="0" Padding="20,24,20,20" Margin="0,0,0,8">
<StackPanel Spacing="14" HorizontalAlignment="Left">
<Border Width="44" Height="44"
Background="{StaticResource ClipForgeAccentBrush}"
CornerRadius="10"
HorizontalAlignment="Left">
<TextBlock Text="&#xE8F1;"
FontFamily="Segoe MDL2 Assets"
FontSize="22"
Foreground="#0A0A0E"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="ClipForge"
FontSize="20"
FontWeight="Bold"
Foreground="{StaticResource ClipForgeAccentBrush}"
CharacterSpacing="60"/>
<TextBlock Text="Capture. Trim. Share."
FontSize="11"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
Opacity="0.9"/>
</StackPanel>
</Border>
<!-- Nav items -->
<StackPanel Grid.Row="1" Spacing="2" Padding="8,8">
<StackPanel Grid.Row="1" Spacing="4" Padding="12,8" VerticalAlignment="Top">
<Button x:Name="NavClips"
HorizontalAlignment="Stretch"
Click="NavClips_Click"
Style="{StaticResource AccentButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="10">
Style="{StaticResource ClipForgeNavButtonSelectedStyle}">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock Text="&#xE8F1;"
FontFamily="Segoe MDL2 Assets"
FontSize="14"/>
FontSize="16"/>
<TextBlock Text="Clips"
FontSize="13"
FontWeight="SemiBold"/>
</StackPanel>
</Button>
<Button x:Name="NavSettings"
HorizontalAlignment="Stretch"
Click="NavSettings_Click">
<StackPanel Orientation="Horizontal" Spacing="10">
Click="NavSettings_Click"
Style="{StaticResource ClipForgeNavButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock Text="&#xE713;"
FontFamily="Segoe MDL2 Assets"
FontSize="14"/>
FontSize="16"/>
<TextBlock Text="Settings"
FontSize="13"
FontWeight="SemiBold"/>
@@ -73,10 +88,15 @@
<!-- Record button -->
<Button x:Name="RecordButton"
Grid.Row="2"
Margin="8"
Margin="12,12,12,20"
HorizontalAlignment="Stretch"
Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="12,10"
Click="RecordButton_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="10">
<Ellipse x:Name="RecordDot"
Width="8" Height="8"
Fill="#FF4757"/>
@@ -84,6 +104,7 @@
Text="CAPTURING"
FontSize="11"
FontWeight="Bold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
CharacterSpacing="40"/>
</StackPanel>
</Button>
@@ -91,7 +112,7 @@
</Border>
<!-- MAIN CONTENT -->
<Grid Grid.Column="1">
<Grid Grid.Column="1" Background="{StaticResource ClipForgeMainBrush}">
<!-- CLIPS PAGE -->
<Grid x:Name="ClipsPage" Visibility="Visible">
@@ -102,37 +123,44 @@
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Padding="24,20,24,0">
<Border Grid.Row="0" Padding="28,24,28,0">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="12"
VerticalAlignment="Bottom">
<TextBlock Text="My Clips"
FontSize="24"
FontWeight="Bold"/>
FontSize="26"
FontWeight="Bold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"/>
<TextBlock x:Name="ClipCountText"
Text="0 clips"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
FontSize="13"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
VerticalAlignment="Bottom"
Margin="0,0,0,3"/>
Margin="0,0,0,4"/>
</StackPanel>
<Button Content="Open Folder"
HorizontalAlignment="Right"
Background="{StaticResource ClipForgeCardBrush}"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,8"
Click="OpenFolder_Click"/>
</Grid>
</Border>
<!-- Toolbar -->
<Border Grid.Row="1" Padding="24,12">
<Border Grid.Row="1" Padding="28,16">
<AutoSuggestBox x:Name="SearchBox"
PlaceholderText="Search clips..."
Width="260"
Width="280"
HorizontalAlignment="Left"
TextChanged="SearchBox_TextChanged"/>
</Border>
<!-- Clip Grid -->
<ScrollViewer Grid.Row="2" Padding="24,0,24,24">
<ScrollViewer Grid.Row="2" Padding="28,0,28,28">
<ItemsControl x:Name="ClipGrid">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -143,11 +171,11 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:ClipFile">
<Border Width="220"
Margin="0,0,12,12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Margin="0,0,14,14"
Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8">
CornerRadius="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="124"/>
@@ -156,8 +184,8 @@
<!-- Thumbnail -->
<Border Grid.Row="0"
Background="#1a1a2e"
CornerRadius="8,8,0,0">
Background="{StaticResource ClipForgeSidebarBrush}"
CornerRadius="10,10,0,0">
<Grid>
<!-- Placeholder icon -->
<TextBlock Text="&#xE786;"
@@ -198,19 +226,23 @@
<TextBlock Text="{x:Bind Title}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
TextTrimming="CharacterEllipsis"/>
<Grid>
<TextBlock Text="{x:Bind CreatedAtDisplay}"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<TextBlock Text="{x:Bind FileSize}"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
HorizontalAlignment="Right"/>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Tag="{x:Bind Path}"
Click="TrimClip_Click"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE8C6;"
FontFamily="Segoe MDL2 Assets"
@@ -218,6 +250,9 @@
</Button>
<Button Tag="{x:Bind Path}"
Click="RenameClip_Click"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE8AC;"
FontFamily="Segoe MDL2 Assets"
@@ -225,6 +260,9 @@
</Button>
<Button Tag="{x:Bind Path}"
Click="DeleteClip_Click"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE74D;"
FontFamily="Segoe MDL2 Assets"
@@ -243,33 +281,36 @@
<!-- SETTINGS PAGE -->
<Grid x:Name="SettingsPage" Visibility="Collapsed">
<ScrollViewer Padding="24">
<StackPanel Spacing="24" MaxWidth="520" HorizontalAlignment="Left">
<ScrollViewer Padding="28">
<StackPanel Spacing="24" MaxWidth="540" HorizontalAlignment="Left">
<TextBlock Text="Settings"
FontSize="24"
FontWeight="Bold"/>
FontSize="26"
FontWeight="Bold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"/>
<!-- Clip Length -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
<Border Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
CornerRadius="10"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE714;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{StaticResource ClipForgeAccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Clip Length"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="How many seconds to save when you press the hotkey."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<Grid>
<Slider x:Name="ClipLengthSlider"
Minimum="10"
@@ -281,7 +322,7 @@
Text="30 seconds"
FontSize="12"
FontWeight="SemiBold"
Foreground="#E8FF47"
Foreground="{StaticResource ClipForgeAccentBrush}"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
</Grid>
@@ -289,25 +330,27 @@
</Border>
<!-- Video Quality -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
<Border Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
CornerRadius="10"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7F4;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{StaticResource ClipForgeAccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Video Quality"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Higher quality means larger file sizes."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<Grid>
<Slider x:Name="QualitySlider"
Minimum="10"
@@ -319,7 +362,7 @@
Text="70%"
FontSize="12"
FontWeight="SemiBold"
Foreground="#E8FF47"
Foreground="{StaticResource ClipForgeAccentBrush}"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
</Grid>
@@ -327,25 +370,27 @@
</Border>
<!-- Framerate -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
<Border Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
CornerRadius="10"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7F8;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{StaticResource ClipForgeAccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Framerate"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Higher framerates are smoother but use more storage."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<ComboBox x:Name="FramerateCombo"
SelectionChanged="FramerateCombo_SelectionChanged">
<ComboBoxItem Content="30 FPS"/>
@@ -355,61 +400,66 @@
</Border>
<!-- Hotkey -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
<Border Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
CornerRadius="10"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE92E;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{StaticResource ClipForgeAccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Clip Hotkey"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="The key combination to save a clip."
<TextBlock Text="Click the key below, then press your desired combination."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Border Background="#1a1a2e"
CornerRadius="6"
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<Button x:Name="HotkeyRecorderButton"
HorizontalAlignment="Left"
Click="HotkeyRecorderButton_Click"
Background="{StaticResource ClipForgeSidebarBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,10"
HorizontalAlignment="Left">
<TextBlock Text="Alt + F9"
FontFamily="Consolas"
FontSize="14"
FontWeight="Bold"
Foreground="#E8FF47"/>
</Border>
<TextBlock Text="Hotkey remapping coming in a future update."
FontSize="11"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"/>
MinWidth="140"
FontFamily="Consolas"
FontSize="14"
FontWeight="Bold"
Foreground="{StaticResource ClipForgeAccentBrush}"
Content="Alt + F9"/>
</StackPanel>
</Border>
<!-- Startup with Windows -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
<Border Background="{StaticResource ClipForgeCardBrush}"
BorderBrush="{StaticResource ClipForgeCardBorderBrush}"
BorderThickness="1"
CornerRadius="8"
CornerRadius="10"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7E8;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
Foreground="{StaticResource ClipForgeAccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Startup"
FontSize="14"
FontWeight="SemiBold"
Foreground="{StaticResource ClipForgeTextPrimaryBrush}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Launch ClipForge automatically when Windows starts."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
Foreground="{StaticResource ClipForgeTextSecondaryBrush}"/>
<ToggleSwitch x:Name="StartupToggle"
OnContent="Enabled"
OffContent="Disabled"
@@ -420,7 +470,7 @@
<!-- Save button -->
<Button x:Name="SaveSettingsButton"
Content="Save Settings"
Style="{StaticResource AccentButtonStyle}"
Style="{StaticResource ClipForgeAccentButtonStyle}"
Click="SaveSettings_Click"
HorizontalAlignment="Left"/>

View File

@@ -1,10 +1,12 @@
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
@@ -16,6 +18,7 @@ namespace ClipForge
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;
@@ -41,6 +44,13 @@ namespace ClipForge
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;
@@ -78,7 +88,10 @@ namespace ClipForge
_hotkeyService = new GlobalHotkeyService();
_hotkeyService.ClipRequested += OnClipRequested;
_hotkeyService.Initialize(hwnd);
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();
@@ -169,6 +182,8 @@ namespace ClipForge
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 ---
@@ -235,17 +250,104 @@ namespace ClipForge
_ = 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)

View File

@@ -11,7 +11,7 @@
<Identity
Name="735fb287-32b4-4217-b84a-302365dc5e23"
Publisher="CN=SCOPEDD"
Version="1.0.0.0" />
Version="0.1.0.0" />
<mp:PhoneIdentity PhoneProductId="735fb287-32b4-4217-b84a-302365dc5e23" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text.Json;
@@ -9,7 +9,10 @@ namespace ClipForge
public int ClipLengthSeconds { get; set; } = 30;
public int VideoQuality { get; set; } = 70;
public int Framerate { get; set; } = 60;
public string HotkeyDisplay { get; set; } = "Alt + F9";
/// <summary>Windows modifier flags: Alt=1, Ctrl=2, Shift=4, Win=8.</summary>
public int HotkeyModifiers { get; set; } = 1; // MOD_ALT
/// <summary>Windows virtual key code (e.g. VK_F9=0x78, VK_PRIOR=0x21 for PgUp).</summary>
public int HotkeyVirtualKey { get; set; } = 0x78; // VK_F9
}
public class SettingsService