Add project files.

This commit is contained in:
Blade / SCOPEDD
2026-02-22 11:31:35 -05:00
parent 3612bf40db
commit c1a425a7ad
84 changed files with 2323 additions and 0 deletions

13
ClipForge.slnx Normal file
View File

@@ -0,0 +1,13 @@
<Solution>
<Configurations>
<Platform Name="ARM64" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="ClipForge/ClipForge.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
<Deploy />
</Project>
</Solution>

16
ClipForge/App.xaml Normal file
View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="ClipForge.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ClipForge">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

46
ClipForge/App.xaml.cs Normal file
View File

@@ -0,0 +1,46 @@
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using WinRT.Interop;
namespace ClipForge
{
public partial class App : Application
{
public MainWindow? MainWindow { get; private set; }
public static DispatcherQueue? MainQueue { get; private set; }
public TrayIconService? TrayIcon { get; private set; }
public App()
{
this.InitializeComponent();
}
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
MainQueue = DispatcherQueue.GetForCurrentThread();
MainWindow = new MainWindow();
MainWindow.Activate();
var hwnd = WindowNative.GetWindowHandle(MainWindow);
TrayIcon = new TrayIconService();
TrayIcon.Initialize(
hwnd,
showWindow: () =>
{
MainQueue?.TryEnqueue(() =>
{
MainWindow?.AppWindow.Show();
MainWindow?.Activate();
});
},
exitApp: () =>
{
MainQueue?.TryEnqueue(() =>
{
TrayIcon?.Dispose();
Microsoft.UI.Xaml.Application.Current.Exit();
});
});
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

57
ClipForge/ClipFile.cs Normal file
View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
namespace ClipForge
{
public class ClipFile : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
private string _thumbnailPath = "";
public string Path { get; set; } = "";
public string Title { get; set; } = "";
public string Duration { get; set; } = "";
public string FileSize { get; set; } = "";
public DateTime CreatedAt { get; set; }
public string CreatedAtDisplay => CreatedAt.ToString("MMM d, yyyy h:mm tt");
public string ThumbnailPath
{
get => _thumbnailPath;
set
{
_thumbnailPath = value;
PropertyChanged?.Invoke(this,
new System.ComponentModel.PropertyChangedEventArgs(
nameof(ThumbnailPath)));
}
}
public bool HasThumbnail => File.Exists(_thumbnailPath);
public static ClipFile FromPath(string path)
{
var info = new FileInfo(path);
var thumbPath = System.IO.Path.ChangeExtension(path, ".thumb.png");
return new ClipFile
{
Path = path,
Title = System.IO.Path.GetFileNameWithoutExtension(path),
FileSize = FormatFileSize(info.Length),
CreatedAt = info.CreationTime,
Duration = "0:30",
ThumbnailPath = thumbPath
};
}
private static string FormatFileSize(long bytes)
{
if (bytes >= 1_073_741_824)
return $"{bytes / 1_073_741_824.0:F1} GB";
if (bytes >= 1_048_576)
return $"{bytes / 1_048_576.0:F0} MB";
return $"{bytes / 1024.0:F0} KB";
}
}
}

View File

@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>ClipForge</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>clipforge.ico</ApplicationIcon>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<IsPublishable>True</IsPublishable>
<GenerateAppInstallerFile>True</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>4A55954F2A73A9D620442C7DFBFC7C95A71D8D24</PackageCertificateThumbprint>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxPackageDir>C:\Users\Blade\Desktop\Clipforge Packaged\V1\</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>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
</PropertyGroup>
<ItemGroup>
<Content Include="ffmpeg.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="clipforge.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="6.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="8.0.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
<PackageReference Include="ScreenRecorderLib" Version="5.0.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
namespace ClipForge
{
public class ClipLibraryService
{
private FileSystemWatcher? _watcher;
public ObservableCollection<ClipFile> Clips { get; } = new();
public string ClipsDirectory { get; } = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.MyVideos), "ClipForge");
public void Initialize()
{
// Create directory if it doesn't exist
if (!Directory.Exists(ClipsDirectory))
Directory.CreateDirectory(ClipsDirectory);
// Load existing clips
LoadExistingClips();
// Watch for new clips being saved
_watcher = new FileSystemWatcher(ClipsDirectory, "*.mp4")
{
EnableRaisingEvents = true,
NotifyFilter = NotifyFilters.FileName
};
_watcher.Created += OnClipCreated;
_watcher.Deleted += OnClipDeleted;
}
private void LoadExistingClips()
{
Clips.Clear();
var files = Directory.GetFiles(ClipsDirectory, "*.mp4");
// Sort newest first
Array.Sort(files, (a, b) =>
File.GetCreationTime(b).CompareTo(File.GetCreationTime(a)));
foreach (var file in files)
Clips.Add(ClipFile.FromPath(file));
}
private void OnClipCreated(object sender, FileSystemEventArgs e)
{
// Must update UI on main thread
App.MainQueue?.TryEnqueue(() =>
{
Clips.Insert(0, ClipFile.FromPath(e.FullPath));
});
}
private void OnClipDeleted(object sender, FileSystemEventArgs e)
{
App.MainQueue?.TryEnqueue(() =>
{
for (int i = Clips.Count - 1; i >= 0; i--)
{
if (Clips[i].Path == e.FullPath)
{
Clips.RemoveAt(i);
break;
}
}
});
}
public void DeleteClip(ClipFile clip)
{
if (File.Exists(clip.Path))
File.Delete(clip.Path);
}
public void OpenInExplorer(ClipFile clip)
{
System.Diagnostics.Process.Start("explorer.exe",
$"/select,\"{clip.Path}\"");
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Runtime.InteropServices;
using Windows.Graphics.DirectX.Direct3D11;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
namespace ClipForge
{
public static class Direct3D11Helper
{
[ComImport]
[Guid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
interface IDirect3DDxgiInterfaceAccess
{
IntPtr GetInterface([In] ref Guid iid);
}
[DllImport("d3d11.dll", EntryPoint = "D3D11CreateDevice",
SetLastError = true,
CharSet = CharSet.Unicode,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
private static extern int D3D11CreateDevice(
IntPtr pAdapter,
uint driverType,
IntPtr software,
uint flags,
IntPtr pFeatureLevels,
uint featureLevels,
uint sdkVersion,
out IntPtr ppDevice,
out uint pFeatureLevel,
out IntPtr ppImmediateContext);
public static IDirect3DDevice CreateDevice()
{
// Create a SharpDX D3D11 device
var d3dDevice = new SharpDX.Direct3D11.Device(
SharpDX.Direct3D.DriverType.Hardware,
DeviceCreationFlags.BgraSupport);
// Get the DXGI device interface from it
var dxgiDevice = d3dDevice.QueryInterface<SharpDX.DXGI.Device>();
// Create a WinRT IDirect3DDevice from the DXGI device
var result = CreateDirect3DDeviceFromDXGIDevice(dxgiDevice.NativePointer);
var winrtDevice = (IDirect3DDevice)Marshal.GetObjectForIUnknown(result);
Marshal.Release(result);
return winrtDevice;
}
[DllImport("d3d11.dll",
EntryPoint = "CreateDirect3D11DeviceFromDXGIDevice",
SetLastError = true,
CharSet = CharSet.Unicode,
ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
private static extern int CreateDirect3D11DeviceFromDXGIDevice(
IntPtr dxgiDevice,
out IntPtr graphicsDevice);
private static IntPtr CreateDirect3DDeviceFromDXGIDevice(IntPtr dxgiDevice)
{
CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice, out IntPtr graphicsDevice);
return graphicsDevice;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX.Direct3D11;
namespace ClipForge
{
public class EncoderService
{
private string _ffmpegPath;
public EncoderService()
{
// FFmpeg binary will sit next to our app executable
_ffmpegPath = Path.Combine(
AppContext.BaseDirectory, "ffmpeg.exe");
}
public async Task SaveClipAsync(
List<Direct3D11CaptureFrame> frames,
string outputPath)
{
if (frames == null || frames.Count == 0) return;
// Create the output directory if it doesn't exist
var directory = Path.GetDirectoryName(outputPath);
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory!);
// For now we'll save a placeholder file confirming
// the frame count — full encoding comes next session
await File.WriteAllTextAsync(outputPath,
$"Clip captured: {frames.Count} frames at {DateTime.Now}");
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
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;
// 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;
public void Initialize(IntPtr hwnd)
{
_hwnd = hwnd;
RegisterHotKey(_hwnd, HOTKEY_CLIP, MOD_ALT, VK_F9);
}
public void ProcessHotkey(int id)
{
if (id == HOTKEY_CLIP)
{
ClipRequested?.Invoke();
}
}
public void Cleanup()
{
UnregisterHotKey(_hwnd, HOTKEY_CLIP);
}
}
}

433
ClipForge/MainWindow.xaml Normal file
View File

@@ -0,0 +1,433 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="ClipForge.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ClipForge"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="ClipForge">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- SIDEBAR -->
<Border Grid.Column="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<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"/>
</Border>
<!-- Nav items -->
<StackPanel Grid.Row="1" Spacing="2" Padding="8,8">
<Button x:Name="NavClips"
HorizontalAlignment="Stretch"
Click="NavClips_Click"
Style="{StaticResource AccentButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE8F1;"
FontFamily="Segoe MDL2 Assets"
FontSize="14"/>
<TextBlock Text="Clips"
FontSize="13"
FontWeight="SemiBold"/>
</StackPanel>
</Button>
<Button x:Name="NavSettings"
HorizontalAlignment="Stretch"
Click="NavSettings_Click">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE713;"
FontFamily="Segoe MDL2 Assets"
FontSize="14"/>
<TextBlock Text="Settings"
FontSize="13"
FontWeight="SemiBold"/>
</StackPanel>
</Button>
</StackPanel>
<!-- Record button -->
<Button x:Name="RecordButton"
Grid.Row="2"
Margin="8"
HorizontalAlignment="Stretch"
Click="RecordButton_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse x:Name="RecordDot"
Width="8" Height="8"
Fill="#FF4757"/>
<TextBlock x:Name="RecordLabel"
Text="CAPTURING"
FontSize="11"
FontWeight="Bold"
CharacterSpacing="40"/>
</StackPanel>
</Button>
</Grid>
</Border>
<!-- MAIN CONTENT -->
<Grid Grid.Column="1">
<!-- CLIPS PAGE -->
<Grid x:Name="ClipsPage" Visibility="Visible">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Padding="24,20,24,0">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="12"
VerticalAlignment="Bottom">
<TextBlock Text="My Clips"
FontSize="24"
FontWeight="Bold"/>
<TextBlock x:Name="ClipCountText"
Text="0 clips"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
VerticalAlignment="Bottom"
Margin="0,0,0,3"/>
</StackPanel>
<Button Content="Open Folder"
HorizontalAlignment="Right"
Click="OpenFolder_Click"/>
</Grid>
</Border>
<!-- Toolbar -->
<Border Grid.Row="1" Padding="24,12">
<AutoSuggestBox x:Name="SearchBox"
PlaceholderText="Search clips..."
Width="260"
HorizontalAlignment="Left"
TextChanged="SearchBox_TextChanged"/>
</Border>
<!-- Clip Grid -->
<ScrollViewer Grid.Row="2" Padding="24,0,24,24">
<ItemsControl x:Name="ClipGrid">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"
ItemWidth="232"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="local:ClipFile">
<Border Width="220"
Margin="0,0,12,12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="124"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Thumbnail -->
<Border Grid.Row="0"
Background="#1a1a2e"
CornerRadius="8,8,0,0">
<Grid>
<!-- Placeholder icon -->
<TextBlock Text="&#xE786;"
FontFamily="Segoe MDL2 Assets"
FontSize="32"
Foreground="#333345"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<!-- Actual thumbnail -->
<Image Stretch="UniformToFill"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image.Source>
<BitmapImage UriSource="{x:Bind ThumbnailPath, Mode=OneWay}"
CreateOptions="IgnoreImageCache"/>
</Image.Source>
</Image>
</Grid>
</Border>
<!-- Duration badge -->
<Border Grid.Row="0"
Background="#99000000"
CornerRadius="4"
Padding="6,2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Margin="0,0,6,6">
<TextBlock Text="{x:Bind Duration}"
FontSize="10"
FontFamily="Consolas"
Foreground="White"/>
</Border>
<!-- Info -->
<Border Grid.Row="1" Padding="10,8">
<StackPanel Spacing="6">
<TextBlock Text="{x:Bind Title}"
FontSize="12"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis"/>
<Grid>
<TextBlock Text="{x:Bind CreatedAtDisplay}"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{x:Bind FileSize}"
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Right"/>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Tag="{x:Bind Path}"
Click="TrimClip_Click"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE8C6;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"/>
</Button>
<Button Tag="{x:Bind Path}"
Click="RenameClip_Click"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE8AC;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"/>
</Button>
<Button Tag="{x:Bind Path}"
Click="DeleteClip_Click"
FontSize="11" Padding="8,4">
<TextBlock Text="&#xE74D;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"/>
</Button>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<!-- SETTINGS PAGE -->
<Grid x:Name="SettingsPage" Visibility="Collapsed">
<ScrollViewer Padding="24">
<StackPanel Spacing="24" MaxWidth="520" HorizontalAlignment="Left">
<TextBlock Text="Settings"
FontSize="24"
FontWeight="Bold"/>
<!-- Clip Length -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE714;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"/>
<TextBlock Text="Clip Length"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="How many seconds to save when you press the hotkey."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Grid>
<Slider x:Name="ClipLengthSlider"
Minimum="10"
Maximum="120"
StepFrequency="5"
Value="30"
ValueChanged="ClipLengthSlider_ValueChanged"/>
<TextBlock x:Name="ClipLengthLabel"
Text="30 seconds"
FontSize="12"
FontWeight="SemiBold"
Foreground="#E8FF47"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</Border>
<!-- Video Quality -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7F4;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"/>
<TextBlock Text="Video Quality"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Higher quality means larger file sizes."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Grid>
<Slider x:Name="QualitySlider"
Minimum="10"
Maximum="100"
StepFrequency="5"
Value="70"
ValueChanged="QualitySlider_ValueChanged"/>
<TextBlock x:Name="QualityLabel"
Text="70%"
FontSize="12"
FontWeight="SemiBold"
Foreground="#E8FF47"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</Border>
<!-- Framerate -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7F8;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"/>
<TextBlock Text="Framerate"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Higher framerates are smoother but use more storage."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<ComboBox x:Name="FramerateCombo"
SelectionChanged="FramerateCombo_SelectionChanged">
<ComboBoxItem Content="30 FPS"/>
<ComboBoxItem Content="60 FPS" IsSelected="True"/>
</ComboBox>
</StackPanel>
</Border>
<!-- Hotkey -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE92E;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"/>
<TextBlock Text="Clip Hotkey"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="The key combination to save a clip."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Border Background="#1a1a2e"
CornerRadius="6"
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}"/>
</StackPanel>
</Border>
<!-- Startup with Windows -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="&#xE7E8;"
FontFamily="Segoe MDL2 Assets"
FontSize="16"
VerticalAlignment="Center"/>
<TextBlock Text="Startup"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="Launch ClipForge automatically when Windows starts."
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<ToggleSwitch x:Name="StartupToggle"
OnContent="Enabled"
OffContent="Disabled"
Toggled="StartupToggle_Toggled"/>
</StackPanel>
</Border>
<!-- Save button -->
<Button x:Name="SaveSettingsButton"
Content="Save Settings"
Style="{StaticResource AccentButtonStyle}"
Click="SaveSettings_Click"
HorizontalAlignment="Left"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,500 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.Win32;
using System;
using System.Linq;
using System.Runtime.InteropServices;
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 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);
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;
_hotkeyService.Initialize(hwnd);
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();
}
// --- 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!");
}
// --- NAV ---
private void NavClips_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Visible;
SettingsPage.Visibility = Visibility.Collapsed;
}
private void NavSettings_Click(object sender, RoutedEventArgs e)
{
ClipsPage.Visibility = Visibility.Collapsed;
SettingsPage.Visibility = Visibility.Visible;
}
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();
}
}
}

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
IgnorableNamespaces="uap rescap systemai">
<Identity
Name="735fb287-32b4-4217-b84a-302365dc5e23"
Publisher="CN=SCOPEDD"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="735fb287-32b4-4217-b84a-302365dc5e23" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>ClipForge</DisplayName>
<PublisherDisplayName>Blade</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="ClipForge"
Description="ClipForge"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" />
<uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badge"/>
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<systemai:Capability Name="systemAIModels"/>
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"ClipForge (Package)": {
"commandName": "MsixPackage"
},
"ClipForge (Unpackaged)": {
"commandName": "Project"
}
}
}

71
ClipForge/RingBuffer.cs Normal file
View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
namespace ClipForge
{
public class RingBuffer<T>
{
private readonly T[] _buffer;
private readonly int _capacity;
private int _head; // where the next write goes
private int _count; // how many slots are filled
private readonly object _lock = new object();
public RingBuffer(int capacity)
{
_capacity = capacity;
_buffer = new T[capacity];
_head = 0;
_count = 0;
}
// Write a new item into the buffer
public void Write(T item)
{
lock (_lock)
{
_buffer[_head] = item;
_head = (_head + 1) % _capacity;
if (_count < _capacity)
_count++;
}
}
// Read the last N items in chronological order
public List<T> ReadLast(int count)
{
lock (_lock)
{
// Can't read more than we have
int available = Math.Min(count, _count);
var result = new List<T>(available);
// Work backwards from the most recent write
int start = (_head - available + _capacity) % _capacity;
for (int i = 0; i < available; i++)
{
result.Add(_buffer[(start + i) % _capacity]);
}
return result;
}
}
// How many items are currently stored
public int Count
{
get { lock (_lock) { return _count; } }
}
// Empty the buffer
public void Clear()
{
lock (_lock)
{
_head = 0;
_count = 0;
}
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.IO;
using System.Threading.Tasks;
using ScreenRecorderLib;
namespace ClipForge
{
public class ScreenCaptureService
{
private Recorder? _recorder;
private string _tempOutputPath;
private bool _isRecording;
public bool IsCapturing => _isRecording;
public ScreenCaptureService()
{
_tempOutputPath = Path.Combine(
Path.GetTempPath(), "clipforge_buffer.mp4");
}
public Task<bool> StartCaptureAsync(IntPtr hwnd)
{
try
{
_recorder = Recorder.CreateRecorder();
_recorder.OnRecordingComplete += OnRecordingComplete;
_recorder.OnRecordingFailed += OnRecordingFailed;
_recorder.OnStatusChanged += OnStatusChanged;
if (File.Exists(_tempOutputPath))
File.Delete(_tempOutputPath);
_recorder.Record(_tempOutputPath);
_isRecording = true;
System.Diagnostics.Debug.WriteLine("Recording started!");
return Task.FromResult(true);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to start: {ex.Message}");
return Task.FromResult(false);
}
}
private void OnStatusChanged(object? sender, RecordingStatusEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Status: {e.Status}");
}
private void OnRecordingFailed(object? sender, RecordingFailedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Failed: {e.Error}");
_isRecording = false;
}
private void OnRecordingComplete(object? sender, RecordingCompleteEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Complete: {e.FilePath}");
_isRecording = false;
}
public async Task<string?> SaveClipAsync(int seconds = 30)
{
if (_recorder == null || !_isRecording)
return null;
try
{
_recorder.Stop();
await Task.Delay(1500);
if (!File.Exists(_tempOutputPath))
return null;
var outputDir = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.MyVideos), "ClipForge");
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
var fileName = $"clip_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.mp4";
var outputPath = Path.Combine(outputDir, fileName);
await TrimToLastSecondsAsync(_tempOutputPath, outputPath, seconds);
// Restart recording for the next clip
if (File.Exists(_tempOutputPath))
File.Delete(_tempOutputPath);
_recorder.Record(_tempOutputPath);
_isRecording = true;
return outputPath;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Save error: {ex.Message}");
return null;
}
}
private async Task TrimToLastSecondsAsync(
string inputPath, string outputPath, int seconds)
{
var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe");
if (!File.Exists(ffmpegPath))
{
// No ffmpeg found — just copy the whole file for now
File.Copy(inputPath, outputPath, true);
return;
}
var args = $"-sseof -{seconds} -i \"{inputPath}\" -c copy \"{outputPath}\"";
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
await Task.Run(() => process.WaitForExit());
}
public void StopCapture()
{
_recorder?.Stop();
_recorder = null;
_isRecording = false;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.IO;
using System.Text.Json;
namespace ClipForge
{
public class AppSettings
{
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";
}
public class SettingsService
{
private static string SettingsPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ClipForge", "settings.json");
public AppSettings Settings { get; private set; } = new();
public void Load()
{
try
{
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
Settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new();
}
}
catch
{
Settings = new AppSettings();
}
}
public void Save()
{
try
{
var dir = Path.GetDirectoryName(SettingsPath)!;
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(Settings,
new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsPath, json);
}
catch { }
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace ClipForge
{
public class ThumbnailService
{
private string _ffmpegPath;
public ThumbnailService()
{
_ffmpegPath = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe");
}
public async Task GenerateThumbnailAsync(ClipFile clip)
{
if (!File.Exists(_ffmpegPath)) return;
if (!File.Exists(clip.Path)) return;
if (File.Exists(clip.ThumbnailPath)) return;
try
{
// Extract frame at 1 second mark
var args = $"-ss 00:00:01 -i \"{clip.Path}\" " +
$"-vframes 1 -q:v 2 " +
$"-vf scale=220:124:force_original_aspect_ratio=increase," +
$"crop=220:124 " +
$"\"{clip.ThumbnailPath}\"";
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = _ffmpegPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true
}
};
process.Start();
await Task.Run(() => process.WaitForExit(5000));
// Notify the clip that thumbnail is ready
if (File.Exists(clip.ThumbnailPath))
{
clip.ThumbnailPath = clip.ThumbnailPath;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Thumbnail error for {clip.Title}: {ex.Message}");
}
}
public async Task GenerateAllThumbnailsAsync(
System.Collections.ObjectModel.ObservableCollection<ClipFile> clips)
{
foreach (var clip in clips)
{
await GenerateThumbnailAsync(clip);
}
}
public void DeleteThumbnail(ClipFile clip)
{
if (File.Exists(clip.ThumbnailPath))
File.Delete(clip.ThumbnailPath);
}
}
}

View File

@@ -0,0 +1,197 @@
using System;
using System.Runtime.InteropServices;
namespace ClipForge
{
public class TrayIconService : IDisposable
{
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpdata);
[DllImport("user32.dll")]
private static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("user32.dll")]
private static extern IntPtr CreatePopupMenu();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem);
[DllImport("user32.dll")]
private static extern int TrackPopupMenu(IntPtr hMenu, uint uFlags, int x, int y, int nReserved, IntPtr hWnd, IntPtr prcRect);
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
// This overload loads from a file path
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr LoadImage(IntPtr hInst, string lpszName,
uint uType, int cxDesired, int cyDesired, uint fuLoad);
// This overload loads a system icon by ID
[DllImport("user32.dll")]
private static extern IntPtr LoadImage(IntPtr hInst, IntPtr lpszName,
uint uType, int cxDesired, int cyDesired, uint fuLoad);
[DllImport("user32.dll")]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, WndProcDelegate newProc);
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NOTIFYICONDATA
{
public uint cbSize;
public IntPtr hWnd;
public uint uID;
public uint uFlags;
public uint uCallbackMessage;
public IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szTip;
public uint dwState;
public uint dwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public uint uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public uint dwInfoFlags;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x; public int y; }
private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private const uint NIM_ADD = 0x00000000;
private const uint NIM_MODIFY = 0x00000001;
private const uint NIM_DELETE = 0x00000002;
private const uint NIF_MESSAGE = 0x00000001;
private const uint NIF_ICON = 0x00000002;
private const uint NIF_TIP = 0x00000004;
private const uint WM_APP = 0x8000;
private const uint WM_TRAYICON = WM_APP + 1;
private const uint WM_RBUTTONUP = 0x0205;
private const uint WM_LBUTTONDBLCLK = 0x0203;
private const uint MF_STRING = 0x00000000;
private const uint MF_SEPARATOR = 0x00000800;
private const uint TPM_RETURNCMD = 0x0100;
private const int IDI_APPLICATION = 32512;
private const uint IMAGE_ICON = 1;
private const uint LR_SHARED = 0x00008000;
private const uint LR_LOADFROMFILE = 0x00000010;
private const int GWLP_WNDPROC = -4;
private const uint CMD_OPEN = 1001;
private const uint CMD_EXIT = 1002;
private NOTIFYICONDATA _iconData;
private IntPtr _hwnd;
private bool _added;
private WndProcDelegate? _wndProc;
private IntPtr _oldWndProc;
private Action? _showWindow;
private Action? _exitApp;
public void Initialize(IntPtr hwnd, Action showWindow, Action exitApp)
{
_hwnd = hwnd;
_showWindow = showWindow;
_exitApp = exitApp;
_wndProc = TrayWndProc;
_oldWndProc = SetWindowLongPtr(_hwnd, GWLP_WNDPROC, _wndProc);
// Try to load our custom icon from the app directory
var iconPath = System.IO.Path.Combine(
AppContext.BaseDirectory, "clipforge.ico");
IntPtr hIcon;
if (System.IO.File.Exists(iconPath))
{
hIcon = LoadImage(IntPtr.Zero, iconPath,
IMAGE_ICON, 16, 16, LR_LOADFROMFILE);
}
else
{
// Fallback to generic Windows icon
hIcon = LoadImage(IntPtr.Zero, new IntPtr(IDI_APPLICATION),
IMAGE_ICON, 16, 16, LR_SHARED);
}
_iconData = new NOTIFYICONDATA
{
cbSize = (uint)Marshal.SizeOf<NOTIFYICONDATA>(),
hWnd = _hwnd,
uID = 1,
uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE,
uCallbackMessage = WM_TRAYICON,
hIcon = hIcon,
szTip = "ClipForge — Recording"
};
Shell_NotifyIcon(NIM_ADD, ref _iconData);
_added = true;
}
private IntPtr TrayWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WM_TRAYICON)
{
uint mouseEvent = (uint)lParam & 0xFFFF;
if (mouseEvent == WM_LBUTTONDBLCLK)
_showWindow?.Invoke();
if (mouseEvent == WM_RBUTTONUP)
ShowContextMenu();
}
return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam);
}
private void ShowContextMenu()
{
var hMenu = CreatePopupMenu();
AppendMenu(hMenu, MF_STRING, CMD_OPEN, "Open ClipForge");
AppendMenu(hMenu, MF_SEPARATOR, 0, "");
AppendMenu(hMenu, MF_STRING, CMD_EXIT, "Exit");
GetCursorPos(out POINT pt);
SetForegroundWindow(_hwnd);
int cmd = TrackPopupMenu(hMenu, TPM_RETURNCMD,
pt.x, pt.y, 0, _hwnd, IntPtr.Zero);
DestroyMenu(hMenu);
if (cmd == CMD_OPEN) _showWindow?.Invoke();
if (cmd == CMD_EXIT) _exitApp?.Invoke();
}
public void SetStatus(string status)
{
if (!_added) return;
_iconData.szTip = $"ClipForge — {status}";
Shell_NotifyIcon(NIM_MODIFY, ref _iconData);
}
public void Dispose()
{
if (_added)
{
Shell_NotifyIcon(NIM_DELETE, ref _iconData);
_added = false;
}
}
}
}

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="ClipForge.TrimmerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="ClipForge Trimmer">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Video Player -->
<Border Grid.Row="0"
Background="#0a0a0f"
Margin="16,16,16,8">
<MediaPlayerElement x:Name="MediaPlayer"
AreTransportControlsEnabled="False"
Stretch="Uniform"/>
</Border>
<!-- Timeline scrubber -->
<Border Grid.Row="1"
Margin="16,0,16,8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="8">
<!-- Main scrubber -->
<Slider x:Name="ScrubberSlider"
Minimum="0"
Maximum="100"
Value="0"
ValueChanged="ScrubberSlider_ValueChanged"/>
<!-- In/Out point controls -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- In point -->
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="IN POINT"
FontSize="10"
FontWeight="Bold"
CharacterSpacing="40"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Name="InPointLabel"
Text="0:00"
FontFamily="Consolas"
FontSize="14"
FontWeight="Bold"
Foreground="#E8FF47"/>
<Button Content="Set"
FontSize="11"
Padding="8,4"
Click="SetInPoint_Click"/>
</StackPanel>
</StackPanel>
<!-- Playback controls -->
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="8"
HorizontalAlignment="Center">
<Button x:Name="PlayButton"
Click="PlayButton_Click"
Width="40" Height="40">
<TextBlock Text="&#xE768;"
FontFamily="Segoe MDL2 Assets"
FontSize="14"/>
</Button>
</StackPanel>
<!-- Out point -->
<StackPanel Grid.Column="2"
Spacing="4"
HorizontalAlignment="Right">
<TextBlock Text="OUT POINT"
FontSize="10"
FontWeight="Bold"
CharacterSpacing="40"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Set"
FontSize="11"
Padding="8,4"
Click="SetOutPoint_Click"/>
<TextBlock x:Name="OutPointLabel"
Text="0:30"
FontFamily="Consolas"
FontSize="14"
FontWeight="Bold"
Foreground="#E8FF47"/>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- Bottom bar -->
<Border Grid.Row="2"
Margin="16,0,16,16"
Padding="16,12">
<Grid>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Duration:"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
VerticalAlignment="Center"/>
<TextBlock x:Name="DurationLabel"
Text="0:00"
FontFamily="Consolas"
FontSize="12"
FontWeight="Bold"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal"
Spacing="8"
HorizontalAlignment="Right">
<Button Content="Cancel"
Click="Cancel_Click"/>
<Button Content="Export Trim"
Style="{StaticResource AccentButtonStyle}"
Click="ExportTrim_Click"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,196 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
namespace ClipForge
{
public sealed partial class TrimmerWindow : Window
{
private string _clipPath;
private TimeSpan _duration;
private TimeSpan _inPoint;
private TimeSpan _outPoint;
private bool _isPlaying;
private bool _scrubbing;
private MediaPlayer _mediaPlayer;
private DispatcherTimer _timer;
public TrimmerWindow(string clipPath)
{
this.InitializeComponent();
_clipPath = clipPath;
this.AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 560));
this.AppWindow.SetPresenter(
Microsoft.UI.Windowing.AppWindowPresenterKind.Overlapped);
LoadVideo();
}
private void LoadVideo()
{
_mediaPlayer = new MediaPlayer();
_mediaPlayer.Source = MediaSource.CreateFromUri(
new Uri(_clipPath));
_mediaPlayer.MediaOpened += OnMediaOpened;
MediaPlayer.SetMediaPlayer(_mediaPlayer);
// Timer to update scrubber position
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(250)
};
_timer.Tick += OnTimerTick;
_timer.Start();
}
private void OnMediaOpened(MediaPlayer sender, object args)
{
_duration = sender.NaturalDuration;
_outPoint = _duration;
DispatcherQueue.TryEnqueue(() =>
{
DurationLabel.Text = FormatTime(_duration);
OutPointLabel.Text = FormatTime(_duration);
ScrubberSlider.Maximum = _duration.TotalSeconds;
});
}
private void OnTimerTick(object? sender, object e)
{
if (_scrubbing || !_isPlaying) return;
ScrubberSlider.Value = _mediaPlayer.Position.TotalSeconds;
}
private void ScrubberSlider_ValueChanged(object sender,
Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
if (_mediaPlayer == null) return;
_scrubbing = true;
_mediaPlayer.Position = TimeSpan.FromSeconds(e.NewValue);
_scrubbing = false;
}
private void PlayButton_Click(object sender, RoutedEventArgs e)
{
if (_isPlaying)
{
_mediaPlayer.Pause();
PlayButton.Content = new TextBlock
{
Text = "\uE768",
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"),
FontSize = 14
};
_isPlaying = false;
}
else
{
_mediaPlayer.Play();
PlayButton.Content = new TextBlock
{
Text = "\uE769",
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Segoe MDL2 Assets"),
FontSize = 14
};
_isPlaying = true;
}
}
private void SetInPoint_Click(object sender, RoutedEventArgs e)
{
_inPoint = _mediaPlayer.Position;
InPointLabel.Text = FormatTime(_inPoint);
}
private void SetOutPoint_Click(object sender, RoutedEventArgs e)
{
_outPoint = _mediaPlayer.Position;
OutPointLabel.Text = FormatTime(_outPoint);
}
private async void ExportTrim_Click(object sender, RoutedEventArgs e)
{
if (_inPoint >= _outPoint)
{
var dialog = new ContentDialog
{
Title = "Invalid Range",
Content = "In point must be before out point.",
CloseButtonText = "OK",
XamlRoot = this.Content.XamlRoot
};
await dialog.ShowAsync();
return;
}
_mediaPlayer.Pause();
_isPlaying = false;
var outputDir = Path.GetDirectoryName(_clipPath)!;
var fileName = Path.GetFileNameWithoutExtension(_clipPath);
var outputPath = Path.Combine(outputDir,
$"{fileName}_trimmed_{DateTime.Now:HHmmss}.mp4");
await TrimWithFfmpegAsync(_clipPath, outputPath,
_inPoint, _outPoint);
var successDialog = new ContentDialog
{
Title = "Export Complete",
Content = $"Saved as {Path.GetFileName(outputPath)}",
CloseButtonText = "OK",
XamlRoot = this.Content.XamlRoot
};
await successDialog.ShowAsync();
this.Close();
}
private async Task TrimWithFfmpegAsync(
string input, string output,
TimeSpan inPoint, TimeSpan outPoint)
{
var ffmpegPath = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe");
if (!File.Exists(ffmpegPath)) return;
var start = inPoint.ToString(@"hh\:mm\:ss\.fff");
var duration = (outPoint - inPoint).ToString(@"hh\:mm\:ss\.fff");
var args = $"-ss {start} -i \"{input}\" -t {duration} -c copy \"{output}\"";
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
await Task.Run(() => process.WaitForExit());
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
_mediaPlayer?.Pause();
_timer?.Stop();
this.Close();
}
private static string FormatTime(TimeSpan t)
{
return t.TotalHours >= 1
? t.ToString(@"h\:mm\:ss")
: t.ToString(@"m\:ss");
}
}
}

19
ClipForge/app.manifest Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ClipForge.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

BIN
ClipForge/clipforge.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

BIN
ClipForge/ffmpeg.exe Normal file

Binary file not shown.