diff --git a/ClipForge.slnx b/ClipForge.slnx
new file mode 100644
index 0000000..59f6575
--- /dev/null
+++ b/ClipForge.slnx
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ClipForge/App.xaml b/ClipForge/App.xaml
new file mode 100644
index 0000000..b3f8d56
--- /dev/null
+++ b/ClipForge/App.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ClipForge/App.xaml.cs b/ClipForge/App.xaml.cs
new file mode 100644
index 0000000..a58166f
--- /dev/null
+++ b/ClipForge/App.xaml.cs
@@ -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();
+ });
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/Assets/BadgeLogo.scale-100.png b/ClipForge/Assets/BadgeLogo.scale-100.png
new file mode 100644
index 0000000..3a4492c
Binary files /dev/null and b/ClipForge/Assets/BadgeLogo.scale-100.png differ
diff --git a/ClipForge/Assets/BadgeLogo.scale-125.png b/ClipForge/Assets/BadgeLogo.scale-125.png
new file mode 100644
index 0000000..78a796b
Binary files /dev/null and b/ClipForge/Assets/BadgeLogo.scale-125.png differ
diff --git a/ClipForge/Assets/BadgeLogo.scale-150.png b/ClipForge/Assets/BadgeLogo.scale-150.png
new file mode 100644
index 0000000..d98dfa6
Binary files /dev/null and b/ClipForge/Assets/BadgeLogo.scale-150.png differ
diff --git a/ClipForge/Assets/BadgeLogo.scale-200.png b/ClipForge/Assets/BadgeLogo.scale-200.png
new file mode 100644
index 0000000..b7d9a76
Binary files /dev/null and b/ClipForge/Assets/BadgeLogo.scale-200.png differ
diff --git a/ClipForge/Assets/BadgeLogo.scale-400.png b/ClipForge/Assets/BadgeLogo.scale-400.png
new file mode 100644
index 0000000..0c8f92c
Binary files /dev/null and b/ClipForge/Assets/BadgeLogo.scale-400.png differ
diff --git a/ClipForge/Assets/LargeTile.scale-100.png b/ClipForge/Assets/LargeTile.scale-100.png
new file mode 100644
index 0000000..1a9bd42
Binary files /dev/null and b/ClipForge/Assets/LargeTile.scale-100.png differ
diff --git a/ClipForge/Assets/LargeTile.scale-125.png b/ClipForge/Assets/LargeTile.scale-125.png
new file mode 100644
index 0000000..d520b68
Binary files /dev/null and b/ClipForge/Assets/LargeTile.scale-125.png differ
diff --git a/ClipForge/Assets/LargeTile.scale-150.png b/ClipForge/Assets/LargeTile.scale-150.png
new file mode 100644
index 0000000..fc25b0d
Binary files /dev/null and b/ClipForge/Assets/LargeTile.scale-150.png differ
diff --git a/ClipForge/Assets/LargeTile.scale-200.png b/ClipForge/Assets/LargeTile.scale-200.png
new file mode 100644
index 0000000..007db29
Binary files /dev/null and b/ClipForge/Assets/LargeTile.scale-200.png differ
diff --git a/ClipForge/Assets/LargeTile.scale-400.png b/ClipForge/Assets/LargeTile.scale-400.png
new file mode 100644
index 0000000..9802e7d
Binary files /dev/null and b/ClipForge/Assets/LargeTile.scale-400.png differ
diff --git a/ClipForge/Assets/SmallTile.scale-100.png b/ClipForge/Assets/SmallTile.scale-100.png
new file mode 100644
index 0000000..09ab901
Binary files /dev/null and b/ClipForge/Assets/SmallTile.scale-100.png differ
diff --git a/ClipForge/Assets/SmallTile.scale-125.png b/ClipForge/Assets/SmallTile.scale-125.png
new file mode 100644
index 0000000..8d254f5
Binary files /dev/null and b/ClipForge/Assets/SmallTile.scale-125.png differ
diff --git a/ClipForge/Assets/SmallTile.scale-150.png b/ClipForge/Assets/SmallTile.scale-150.png
new file mode 100644
index 0000000..d73d12a
Binary files /dev/null and b/ClipForge/Assets/SmallTile.scale-150.png differ
diff --git a/ClipForge/Assets/SmallTile.scale-200.png b/ClipForge/Assets/SmallTile.scale-200.png
new file mode 100644
index 0000000..4fa45c9
Binary files /dev/null and b/ClipForge/Assets/SmallTile.scale-200.png differ
diff --git a/ClipForge/Assets/SmallTile.scale-400.png b/ClipForge/Assets/SmallTile.scale-400.png
new file mode 100644
index 0000000..3c6f165
Binary files /dev/null and b/ClipForge/Assets/SmallTile.scale-400.png differ
diff --git a/ClipForge/Assets/SplashScreen.png b/ClipForge/Assets/SplashScreen.png
new file mode 100644
index 0000000..c2a9d70
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.png differ
diff --git a/ClipForge/Assets/SplashScreen.scale-100.png b/ClipForge/Assets/SplashScreen.scale-100.png
new file mode 100644
index 0000000..707aad7
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.scale-100.png differ
diff --git a/ClipForge/Assets/SplashScreen.scale-125.png b/ClipForge/Assets/SplashScreen.scale-125.png
new file mode 100644
index 0000000..4f995a4
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.scale-125.png differ
diff --git a/ClipForge/Assets/SplashScreen.scale-150.png b/ClipForge/Assets/SplashScreen.scale-150.png
new file mode 100644
index 0000000..aedd244
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.scale-150.png differ
diff --git a/ClipForge/Assets/SplashScreen.scale-200.png b/ClipForge/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000..9facdc8
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.scale-200.png differ
diff --git a/ClipForge/Assets/SplashScreen.scale-400.png b/ClipForge/Assets/SplashScreen.scale-400.png
new file mode 100644
index 0000000..aa4db3c
Binary files /dev/null and b/ClipForge/Assets/SplashScreen.scale-400.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.png b/ClipForge/Assets/Square150x150Logo.png
new file mode 100644
index 0000000..6fe14b5
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.scale-100.png b/ClipForge/Assets/Square150x150Logo.scale-100.png
new file mode 100644
index 0000000..6010720
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.scale-100.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.scale-125.png b/ClipForge/Assets/Square150x150Logo.scale-125.png
new file mode 100644
index 0000000..9ecae74
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.scale-125.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.scale-150.png b/ClipForge/Assets/Square150x150Logo.scale-150.png
new file mode 100644
index 0000000..6bf0971
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.scale-150.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.scale-200.png b/ClipForge/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000..c079976
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.scale-200.png differ
diff --git a/ClipForge/Assets/Square150x150Logo.scale-400.png b/ClipForge/Assets/Square150x150Logo.scale-400.png
new file mode 100644
index 0000000..93eb0d4
Binary files /dev/null and b/ClipForge/Assets/Square150x150Logo.scale-400.png differ
diff --git a/ClipForge/Assets/Square310x310Logo.png b/ClipForge/Assets/Square310x310Logo.png
new file mode 100644
index 0000000..e7790f2
Binary files /dev/null and b/ClipForge/Assets/Square310x310Logo.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png
new file mode 100644
index 0000000..d4c2919
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png
new file mode 100644
index 0000000..d3addbf
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png
new file mode 100644
index 0000000..1c9405e
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png
new file mode 100644
index 0000000..ad6e73f
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png
new file mode 100644
index 0000000..2dfe5dc
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-16.png
new file mode 100644
index 0000000..d4c2919
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-24.png b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-24.png
new file mode 100644
index 0000000..d3addbf
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-24.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-256.png
new file mode 100644
index 0000000..1c9405e
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-32.png
new file mode 100644
index 0000000..ad6e73f
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-48.png
new file mode 100644
index 0000000..2dfe5dc
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.png b/ClipForge/Assets/Square44x44Logo.png
new file mode 100644
index 0000000..842c94a
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.scale-100.png b/ClipForge/Assets/Square44x44Logo.scale-100.png
new file mode 100644
index 0000000..7f261c9
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.scale-100.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.scale-125.png b/ClipForge/Assets/Square44x44Logo.scale-125.png
new file mode 100644
index 0000000..226e2be
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.scale-125.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.scale-150.png b/ClipForge/Assets/Square44x44Logo.scale-150.png
new file mode 100644
index 0000000..41ed872
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.scale-150.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.scale-200.png b/ClipForge/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000..c2f22df
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.scale-200.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.scale-400.png b/ClipForge/Assets/Square44x44Logo.scale-400.png
new file mode 100644
index 0000000..353547a
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.scale-400.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.targetsize-16.png b/ClipForge/Assets/Square44x44Logo.targetsize-16.png
new file mode 100644
index 0000000..f4a0270
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.targetsize-16.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.targetsize-24.png b/ClipForge/Assets/Square44x44Logo.targetsize-24.png
new file mode 100644
index 0000000..17e4837
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.targetsize-24.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.targetsize-256.png b/ClipForge/Assets/Square44x44Logo.targetsize-256.png
new file mode 100644
index 0000000..9780fa4
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.targetsize-256.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.targetsize-32.png b/ClipForge/Assets/Square44x44Logo.targetsize-32.png
new file mode 100644
index 0000000..a61fde5
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.targetsize-32.png differ
diff --git a/ClipForge/Assets/Square44x44Logo.targetsize-48.png b/ClipForge/Assets/Square44x44Logo.targetsize-48.png
new file mode 100644
index 0000000..c9c46dd
Binary files /dev/null and b/ClipForge/Assets/Square44x44Logo.targetsize-48.png differ
diff --git a/ClipForge/Assets/StoreLogo.backup.png b/ClipForge/Assets/StoreLogo.backup.png
new file mode 100644
index 0000000..4ee6d9f
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.backup.png differ
diff --git a/ClipForge/Assets/StoreLogo.scale-100.png b/ClipForge/Assets/StoreLogo.scale-100.png
new file mode 100644
index 0000000..08aeadf
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.scale-100.png differ
diff --git a/ClipForge/Assets/StoreLogo.scale-125.png b/ClipForge/Assets/StoreLogo.scale-125.png
new file mode 100644
index 0000000..d4f19b4
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.scale-125.png differ
diff --git a/ClipForge/Assets/StoreLogo.scale-150.png b/ClipForge/Assets/StoreLogo.scale-150.png
new file mode 100644
index 0000000..2d05846
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.scale-150.png differ
diff --git a/ClipForge/Assets/StoreLogo.scale-200.png b/ClipForge/Assets/StoreLogo.scale-200.png
new file mode 100644
index 0000000..65eece9
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.scale-200.png differ
diff --git a/ClipForge/Assets/StoreLogo.scale-400.png b/ClipForge/Assets/StoreLogo.scale-400.png
new file mode 100644
index 0000000..6b587d4
Binary files /dev/null and b/ClipForge/Assets/StoreLogo.scale-400.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.png b/ClipForge/Assets/Wide310x150Logo.png
new file mode 100644
index 0000000..3817bc3
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.scale-100.png b/ClipForge/Assets/Wide310x150Logo.scale-100.png
new file mode 100644
index 0000000..552d04e
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.scale-100.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.scale-125.png b/ClipForge/Assets/Wide310x150Logo.scale-125.png
new file mode 100644
index 0000000..2a39f95
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.scale-125.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.scale-150.png b/ClipForge/Assets/Wide310x150Logo.scale-150.png
new file mode 100644
index 0000000..c272211
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.scale-150.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.scale-200.png b/ClipForge/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000..707aad7
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/ClipForge/Assets/Wide310x150Logo.scale-400.png b/ClipForge/Assets/Wide310x150Logo.scale-400.png
new file mode 100644
index 0000000..9facdc8
Binary files /dev/null and b/ClipForge/Assets/Wide310x150Logo.scale-400.png differ
diff --git a/ClipForge/ClipFile.cs b/ClipForge/ClipFile.cs
new file mode 100644
index 0000000..2b8420a
--- /dev/null
+++ b/ClipForge/ClipFile.cs
@@ -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";
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/ClipForge.csproj b/ClipForge/ClipForge.csproj
new file mode 100644
index 0000000..af21841
--- /dev/null
+++ b/ClipForge/ClipForge.csproj
@@ -0,0 +1,52 @@
+
+
+ WinExe
+ net8.0-windows10.0.19041.0
+ 10.0.17763.0
+ ClipForge
+ app.manifest
+ clipforge.ico
+ x86;x64;ARM64
+ win-x86;win-x64;win-arm64
+ true
+ true
+ enable
+ disable
+ True
+ True
+ True
+ 4A55954F2A73A9D620442C7DFBFC7C95A71D8D24
+ SHA256
+ True
+ C:\Users\Blade\Desktop\Clipforge Packaged\V1\
+ False
+ True
+ Auto
+ x64
+ J:\Projects\ClipForge\ClipForge\bin\x64\Release\net8.0-windows10.0.19041.0
+ 0
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClipForge/ClipLibraryService.cs b/ClipForge/ClipLibraryService.cs
new file mode 100644
index 0000000..f269a35
--- /dev/null
+++ b/ClipForge/ClipLibraryService.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+
+namespace ClipForge
+{
+ public class ClipLibraryService
+ {
+ private FileSystemWatcher? _watcher;
+ public ObservableCollection 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}\"");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/Direct3D11Helper.cs b/ClipForge/Direct3D11Helper.cs
new file mode 100644
index 0000000..a9445ed
--- /dev/null
+++ b/ClipForge/Direct3D11Helper.cs
@@ -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();
+
+ // 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/EncoderService.cs b/ClipForge/EncoderService.cs
new file mode 100644
index 0000000..854d743
--- /dev/null
+++ b/ClipForge/EncoderService.cs
@@ -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 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}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/GlobalHotkeyService.cs b/ClipForge/GlobalHotkeyService.cs
new file mode 100644
index 0000000..d786b51
--- /dev/null
+++ b/ClipForge/GlobalHotkeyService.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/MainWindow.xaml b/ClipForge/MainWindow.xaml
new file mode 100644
index 0000000..3e3959c
--- /dev/null
+++ b/ClipForge/MainWindow.xaml
@@ -0,0 +1,433 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClipForge/MainWindow.xaml.cs b/ClipForge/MainWindow.xaml.cs
new file mode 100644
index 0000000..4320aad
--- /dev/null
+++ b/ClipForge/MainWindow.xaml.cs
@@ -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();
+ storyboardOut.Completed += (s, e) => tcs.SetResult(true);
+ storyboardOut.Begin();
+ await tcs.Task;
+
+ overlayWindow.Close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/Package.appxmanifest b/ClipForge/Package.appxmanifest
new file mode 100644
index 0000000..ca70092
--- /dev/null
+++ b/ClipForge/Package.appxmanifest
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+ ClipForge
+ Blade
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ClipForge/Properties/launchSettings.json b/ClipForge/Properties/launchSettings.json
new file mode 100644
index 0000000..d5e254c
--- /dev/null
+++ b/ClipForge/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "ClipForge (Package)": {
+ "commandName": "MsixPackage"
+ },
+ "ClipForge (Unpackaged)": {
+ "commandName": "Project"
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/RingBuffer.cs b/ClipForge/RingBuffer.cs
new file mode 100644
index 0000000..d6e857d
--- /dev/null
+++ b/ClipForge/RingBuffer.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+
+namespace ClipForge
+{
+ public class RingBuffer
+ {
+ 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 ReadLast(int count)
+ {
+ lock (_lock)
+ {
+ // Can't read more than we have
+ int available = Math.Min(count, _count);
+ var result = new List(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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/ScreenCaptureService.cs b/ClipForge/ScreenCaptureService.cs
new file mode 100644
index 0000000..395bbd8
--- /dev/null
+++ b/ClipForge/ScreenCaptureService.cs
@@ -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 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 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/SettingsService.cs b/ClipForge/SettingsService.cs
new file mode 100644
index 0000000..929eb3c
--- /dev/null
+++ b/ClipForge/SettingsService.cs
@@ -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(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 { }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/ThumbnailService.cs b/ClipForge/ThumbnailService.cs
new file mode 100644
index 0000000..ad13e22
--- /dev/null
+++ b/ClipForge/ThumbnailService.cs
@@ -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 clips)
+ {
+ foreach (var clip in clips)
+ {
+ await GenerateThumbnailAsync(clip);
+ }
+ }
+
+ public void DeleteThumbnail(ClipFile clip)
+ {
+ if (File.Exists(clip.ThumbnailPath))
+ File.Delete(clip.ThumbnailPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/TrayIconService.cs b/ClipForge/TrayIconService.cs
new file mode 100644
index 0000000..6d9a662
--- /dev/null
+++ b/ClipForge/TrayIconService.cs
@@ -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(),
+ 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/TrimmerWindow.xaml b/ClipForge/TrimmerWindow.xaml
new file mode 100644
index 0000000..eb9e1fc
--- /dev/null
+++ b/ClipForge/TrimmerWindow.xaml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ClipForge/TrimmerWindow.xaml.cs b/ClipForge/TrimmerWindow.xaml.cs
new file mode 100644
index 0000000..8ac17e3
--- /dev/null
+++ b/ClipForge/TrimmerWindow.xaml.cs
@@ -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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ClipForge/app.manifest b/ClipForge/app.manifest
new file mode 100644
index 0000000..1db6bfb
--- /dev/null
+++ b/ClipForge/app.manifest
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2
+
+
+
\ No newline at end of file
diff --git a/ClipForge/clipforge.ico b/ClipForge/clipforge.ico
new file mode 100644
index 0000000..52a9243
Binary files /dev/null and b/ClipForge/clipforge.ico differ
diff --git a/ClipForge/ffmpeg.exe b/ClipForge/ffmpeg.exe
new file mode 100644
index 0000000..453f05c
Binary files /dev/null and b/ClipForge/ffmpeg.exe differ