Add project files.
13
ClipForge.slnx
Normal 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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
ClipForge/Assets/BadgeLogo.scale-100.png
Normal file
|
After Width: | Height: | Size: 137 B |
BIN
ClipForge/Assets/BadgeLogo.scale-125.png
Normal file
|
After Width: | Height: | Size: 146 B |
BIN
ClipForge/Assets/BadgeLogo.scale-150.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
ClipForge/Assets/BadgeLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 197 B |
BIN
ClipForge/Assets/BadgeLogo.scale-400.png
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
ClipForge/Assets/LargeTile.scale-100.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
ClipForge/Assets/LargeTile.scale-125.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
ClipForge/Assets/LargeTile.scale-150.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
ClipForge/Assets/LargeTile.scale-200.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ClipForge/Assets/LargeTile.scale-400.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
ClipForge/Assets/SmallTile.scale-100.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
ClipForge/Assets/SmallTile.scale-125.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
ClipForge/Assets/SmallTile.scale-150.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
ClipForge/Assets/SmallTile.scale-200.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
ClipForge/Assets/SmallTile.scale-400.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
ClipForge/Assets/SplashScreen.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
ClipForge/Assets/SplashScreen.scale-100.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
ClipForge/Assets/SplashScreen.scale-125.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
ClipForge/Assets/SplashScreen.scale-150.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
ClipForge/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
ClipForge/Assets/SplashScreen.scale-400.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
ClipForge/Assets/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
ClipForge/Assets/Square150x150Logo.scale-100.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ClipForge/Assets/Square150x150Logo.scale-125.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
ClipForge/Assets/Square150x150Logo.scale-150.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
ClipForge/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
ClipForge/Assets/Square150x150Logo.scale-400.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
ClipForge/Assets/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
BIN
ClipForge/Assets/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
ClipForge/Assets/Square44x44Logo.scale-100.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ClipForge/Assets/Square44x44Logo.scale-125.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
ClipForge/Assets/Square44x44Logo.scale-150.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
ClipForge/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
ClipForge/Assets/Square44x44Logo.scale-400.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
ClipForge/Assets/Square44x44Logo.targetsize-16.png
Normal file
|
After Width: | Height: | Size: 527 B |
BIN
ClipForge/Assets/Square44x44Logo.targetsize-24.png
Normal file
|
After Width: | Height: | Size: 843 B |
BIN
ClipForge/Assets/Square44x44Logo.targetsize-256.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
ClipForge/Assets/Square44x44Logo.targetsize-32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
ClipForge/Assets/Square44x44Logo.targetsize-48.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
ClipForge/Assets/StoreLogo.backup.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
ClipForge/Assets/StoreLogo.scale-100.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
ClipForge/Assets/StoreLogo.scale-125.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
ClipForge/Assets/StoreLogo.scale-150.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
ClipForge/Assets/StoreLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
ClipForge/Assets/StoreLogo.scale-400.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.scale-100.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.scale-125.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.scale-150.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
ClipForge/Assets/Wide310x150Logo.scale-400.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
57
ClipForge/ClipFile.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
52
ClipForge/ClipForge.csproj
Normal 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>
|
||||
85
ClipForge/ClipLibraryService.cs
Normal 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}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
71
ClipForge/Direct3D11Helper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ClipForge/EncoderService.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
49
ClipForge/GlobalHotkeyService.cs
Normal 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
@@ -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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12"/>
|
||||
</Button>
|
||||
<Button Tag="{x:Bind Path}"
|
||||
Click="RenameClip_Click"
|
||||
FontSize="11" Padding="8,4">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12"/>
|
||||
</Button>
|
||||
<Button Tag="{x:Bind Path}"
|
||||
Click="DeleteClip_Click"
|
||||
FontSize="11" Padding="8,4">
|
||||
<TextBlock Text=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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>
|
||||
500
ClipForge/MainWindow.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
ClipForge/Package.appxmanifest
Normal 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>
|
||||
10
ClipForge/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"ClipForge (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
},
|
||||
"ClipForge (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
71
ClipForge/RingBuffer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
ClipForge/ScreenCaptureService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
ClipForge/SettingsService.cs
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
74
ClipForge/ThumbnailService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
197
ClipForge/TrayIconService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
ClipForge/TrimmerWindow.xaml
Normal 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=""
|
||||
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>
|
||||
196
ClipForge/TrimmerWindow.xaml.cs
Normal 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
@@ -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
|
After Width: | Height: | Size: 226 B |