Projektdateien hinzufügen.

This commit is contained in:
2025-10-23 18:57:34 +02:00
parent 0ded3af381
commit fa4c2b14d3
28 changed files with 2580 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

363
.gitignore vendored Normal file
View File

@@ -0,0 +1,363 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

3
MaxSlurper.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="MaxSlurper/MaxSlurper.csproj" />
</Solution>

29
MaxSlurper/App.xaml Normal file
View File

@@ -0,0 +1,29 @@
<Application x:Class="MaxSlurper.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MaxSlurper"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Wpf.Ui FluentUI theme resources -->
<ui:ThemesDictionary Theme="Light" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
<!-- Custom app-specific styles -->
<Style TargetType="TextBlock" x:Key="HeaderText">
<Setter Property="FontSize" Value="20" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
</Style>
<Style TargetType="TextBlock" x:Key="SubtleText">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

19
MaxSlurper/App.xaml.cs Normal file
View File

@@ -0,0 +1,19 @@
using System.Configuration;
using System.Data;
using Wpf.Ui.Appearance;
namespace MaxSlurper
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : System.Windows.Application
{
protected override void OnStartup(System.Windows.StartupEventArgs e)
{
base.OnStartup(e);
// The theme will be applied automatically when the window is shown
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,7 @@
namespace MaxSlurper.Audio
{
public interface IAudioPlayer
{
void Play(string filename);
}
}

View File

@@ -0,0 +1,22 @@
using System.Media;
using System.IO;
namespace MaxSlurper.Audio
{
public class SimpleAudioPlayer : IAudioPlayer
{
public void Play(string filename)
{
try
{
var path = Path.Combine(System.AppContext.BaseDirectory, filename);
if (File.Exists(path))
{
using var player = new SoundPlayer(path);
player.Play();
}
}
catch { }
}
}
}

View File

@@ -0,0 +1,193 @@
<ui:FluentWindow x:Class="MaxSlurper.Controls.ColorPickerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="Farbe aussuche"
Height="500"
Width="400"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Topmost="True"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
Icon="/logo.ico">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<ui:TitleBar Grid.Row="0"
Title="Farbe aussuche"
ShowMaximize="False"
ShowMinimize="False"
Icon="/logo.png" />
<!-- Color Preview -->
<Border Grid.Row="1"
Height="100"
Background="{Binding SelectedColorBrush}"
CornerRadius="12"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
Margin="20,8,20,16">
<Border.Effect>
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.2"/>
</Border.Effect>
</Border>
<!-- Color Values -->
<StackPanel Grid.Row="2" Margin="20,0,20,16">
<TextBlock Text="{Binding SelectedHex}"
FontSize="22"
FontWeight="SemiBold"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,4"/>
<TextBlock Text="{Binding SelectedRgb}"
FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"/>
</StackPanel>
<!-- Hue Slider -->
<Grid Grid.Row="3" Margin="20,0,20,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Farbton"
FontSize="13"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,6"/>
<Border Grid.Row="1"
Height="36"
CornerRadius="8"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#FF0000" Offset="0.0"/>
<GradientStop Color="#FFFF00" Offset="0.167"/>
<GradientStop Color="#00FF00" Offset="0.333"/>
<GradientStop Color="#00FFFF" Offset="0.5"/>
<GradientStop Color="#0000FF" Offset="0.667"/>
<GradientStop Color="#FF00FF" Offset="0.833"/>
<GradientStop Color="#FF0000" Offset="1.0"/>
</LinearGradientBrush>
</Border.Background>
<Canvas x:Name="HueCanvas"
Background="Transparent"
MouseLeftButtonDown="HueCanvas_MouseLeftButtonDown"
MouseMove="HueCanvas_MouseMove"
Cursor="Hand">
<!-- Hue Indicator -->
<Border x:Name="HueIndicator"
Width="4"
Height="36"
Background="White"
CornerRadius="2"
IsHitTestVisible="False">
<Border.Effect>
<DropShadowEffect BlurRadius="4" ShadowDepth="0" Opacity="0.8" Color="Black"/>
</Border.Effect>
</Border>
</Canvas>
</Border>
</Grid>
<!-- Saturation/Value Label -->
<TextBlock Grid.Row="4"
Text="Sättigung &amp; Helligkeit"
FontSize="13"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="20,0,20,6"/>
<!-- Saturation/Value Picker -->
<Border Grid.Row="5"
CornerRadius="12"
Margin="20,0,20,16"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
ClipToBounds="True"
MinHeight="200">
<Grid>
<!-- Base hue color -->
<Border x:Name="HueBackground" Background="Red"/>
<!-- White to transparent (Saturation) -->
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#00FFFFFF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<!-- Transparent to black (Value) -->
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#00000000" Offset="0"/>
<GradientStop Color="#FF000000" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<Canvas x:Name="SvCanvas"
Background="Transparent"
MouseLeftButtonDown="SvCanvas_MouseLeftButtonDown"
MouseMove="SvCanvas_MouseMove"
Cursor="Cross">
<!-- Selection Indicator -->
<Ellipse x:Name="SvIndicator"
Width="20"
Height="20"
Stroke="White"
StrokeThickness="3"
Fill="Transparent"
IsHitTestVisible="False">
<Ellipse.Effect>
<DropShadowEffect BlurRadius="6" ShadowDepth="0" Opacity="0.8" Color="Black"/>
</Ellipse.Effect>
</Ellipse>
</Canvas>
</Grid>
</Border>
<!-- Buttons -->
<Grid Grid.Row="6" Margin="20,0,20,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ui:Button Content="Lass ma"
Height="44"
Margin="0,0,8,0"
Click="CancelButton_Click"
Appearance="Secondary"/>
<ui:Button Grid.Column="1"
Content="Kopieren &amp; Tschüss"
Height="44"
Padding="24,0"
Click="OkButton_Click"
Appearance="Primary"/>
</Grid>
</Grid>
</ui:FluentWindow>

View File

@@ -0,0 +1,262 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using Wpf.Ui.Controls;
namespace MaxSlurper.Controls
{
public partial class ColorPickerWindow : FluentWindow, INotifyPropertyChanged
{
private double _hue = 0;
private double _saturation = 1;
private double _value = 1;
private bool _isDraggingHue = false;
private bool _isDraggingSv = false;
public event PropertyChangedEventHandler? PropertyChanged;
private System.Windows.Media.Color _selectedColor = Colors.Red;
public System.Windows.Media.Color SelectedColor
{
get => _selectedColor;
set
{
_selectedColor = value;
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedColorBrush));
OnPropertyChanged(nameof(SelectedHex));
OnPropertyChanged(nameof(SelectedRgb));
}
}
public SolidColorBrush SelectedColorBrush => new SolidColorBrush(SelectedColor);
public string SelectedHex => $"#{SelectedColor.R:X2}{SelectedColor.G:X2}{SelectedColor.B:X2}";
public string SelectedRgb => $"RGB({SelectedColor.R}, {SelectedColor.G}, {SelectedColor.B})";
public ColorPickerWindow()
{
InitializeComponent();
DataContext = this;
Loaded += ColorPickerWindow_Loaded;
}
public ColorPickerWindow(System.Windows.Media.Color initialColor) : this()
{
SelectedColor = initialColor;
ColorToHsv(initialColor, out _hue, out _saturation, out _value);
}
private void ColorPickerWindow_Loaded(object sender, RoutedEventArgs e)
{
UpdateHueBackground();
UpdateHueIndicatorPosition();
UpdateSvIndicatorPosition();
}
private void HueCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDraggingHue = true;
HueCanvas.CaptureMouse();
UpdateHueFromMouse(e.GetPosition(HueCanvas));
}
private void HueCanvas_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (_isDraggingHue)
{
UpdateHueFromMouse(e.GetPosition(HueCanvas));
}
}
private void SvCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDraggingSv = true;
SvCanvas.CaptureMouse();
UpdateSvFromMouse(e.GetPosition(SvCanvas));
}
private void SvCanvas_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (_isDraggingSv)
{
UpdateSvFromMouse(e.GetPosition(SvCanvas));
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (_isDraggingHue)
{
_isDraggingHue = false;
HueCanvas.ReleaseMouseCapture();
}
if (_isDraggingSv)
{
_isDraggingSv = false;
SvCanvas.ReleaseMouseCapture();
}
}
private void UpdateHueFromMouse(System.Windows.Point position)
{
double width = HueCanvas.ActualWidth;
if (width <= 0) return;
_hue = Math.Clamp(position.X / width, 0, 1) * 360;
UpdateHueBackground();
UpdateHueIndicatorPosition();
UpdateColorFromHsv();
}
private void UpdateSvFromMouse(System.Windows.Point position)
{
double width = SvCanvas.ActualWidth;
double height = SvCanvas.ActualHeight;
if (width <= 0 || height <= 0) return;
_saturation = Math.Clamp(position.X / width, 0, 1);
_value = 1 - Math.Clamp(position.Y / height, 0, 1);
UpdateSvIndicatorPosition();
UpdateColorFromHsv();
}
private void UpdateHueBackground()
{
var hueColor = HsvToColor(_hue, 1, 1);
HueBackground.Background = new SolidColorBrush(hueColor);
}
private void UpdateHueIndicatorPosition()
{
if (HueCanvas.ActualWidth <= 0) return;
double x = (_hue / 360.0) * HueCanvas.ActualWidth;
Canvas.SetLeft(HueIndicator, x - 2); // Center the 4px wide indicator
}
private void UpdateSvIndicatorPosition()
{
if (SvCanvas.ActualWidth <= 0 || SvCanvas.ActualHeight <= 0) return;
double x = _saturation * SvCanvas.ActualWidth;
double y = (1 - _value) * SvCanvas.ActualHeight;
Canvas.SetLeft(SvIndicator, x - 10); // Changed from 8 to 10 (half of 20)
Canvas.SetTop(SvIndicator, y - 10); // Changed from 8 to 10 (half of 20)
}
private void UpdateColorFromHsv()
{
SelectedColor = HsvToColor(_hue, _saturation, _value);
}
private static System.Windows.Media.Color HsvToColor(double h, double s, double v)
{
double c = v * s;
double x = c * (1 - Math.Abs((h / 60) % 2 - 1));
double m = v - c;
double r = 0, g = 0, b = 0;
if (h >= 0 && h < 60)
{
r = c; g = x; b = 0;
}
else if (h >= 60 && h < 120)
{
r = x; g = c; b = 0;
}
else if (h >= 120 && h < 180)
{
r = 0; g = c; b = x;
}
else if (h >= 180 && h < 240)
{
r = 0; g = x; b = c;
}
else if (h >= 240 && h < 300)
{
r = x; g = 0; b = c;
}
else if (h >= 300 && h < 360)
{
r = c; g = 0; b = x;
}
return System.Windows.Media.Color.FromRgb(
(byte)Math.Round((r + m) * 255),
(byte)Math.Round((g + m) * 255),
(byte)Math.Round((b + m) * 255)
);
}
private static void ColorToHsv(System.Windows.Media.Color color, out double h, out double s, out double v)
{
double r = color.R / 255.0;
double g = color.G / 255.0;
double b = color.B / 255.0;
double max = Math.Max(r, Math.Max(g, b));
double min = Math.Min(r, Math.Min(g, b));
double delta = max - min;
// Hue
if (delta == 0)
{
h = 0;
}
else if (max == r)
{
h = 60 * (((g - b) / delta) % 6);
}
else if (max == g)
{
h = 60 * (((b - r) / delta) + 2);
}
else
{
h = 60 * (((r - g) / delta) + 4);
}
if (h < 0) h += 360;
// Saturation
s = max == 0 ? 0 : delta / max;
// Value
v = max;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,168 @@
<ui:FluentWindow x:Class="MaxSlurper.Controls.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="Einstellungen"
Height="520"
Width="400"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Topmost="False"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowCornerPreference="Round"
Icon="/logo.ico">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Title bar -->
<ui:TitleBar Grid.Row="0"
Title="Einstellungen"
ShowMaximize="False"
ShowMinimize="False"
Icon="/logo.png" />
<!-- Main content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
Margin="24,16,24,16">
<StackPanel>
<!-- Hotkey Settings Card -->
<ui:Card
Margin="0,0,0,16"
Padding="20">
<StackPanel>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ui:SymbolIcon Symbol="Keyboard24"
FontSize="24"
VerticalAlignment="Center"
Margin="0,0,12,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="Tastenkombination"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
<TextBlock
TextWrapping="Wrap"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,2,0,0"><Run Language="de-de" Text="H"/><Run Text="otkey zum Farben schlürfen"/></TextBlock>
</StackPanel>
</Grid>
<TextBlock Text="Aktuelle Tastenkombination"
FontSize="13"
FontWeight="Medium"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,8"/>
<!-- Hotkey Display -->
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12"
Margin="0,0,0,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding CurrentHotkey}"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
<ui:Button Grid.Column="1"
Content="Ändern"
Appearance="Secondary"
Click="ChangeHotkey_Click"
Padding="16,8"/>
</Grid>
</Border>
<!-- Instructions -->
</StackPanel>
</ui:Card>
<!-- About Card -->
<ui:Card Padding="20">
<StackPanel>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ui:SymbolIcon Symbol="Info24"
FontSize="24"
VerticalAlignment="Center"
Margin="0,0,12,0"/>
<TextBlock Grid.Column="1"
Text="Über MaxSlurper"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
</Grid>
<TextBlock Text="DER ColorPicker für Max"
TextWrapping="Wrap"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,0,8"/>
<TextBlock TextWrapping="Wrap"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}">
<Run Text="Gemacht mit "/>
<Run Text="♥" Foreground="Red" FontSize="14"/>
<Run Text=" von Mathias"/>
</TextBlock>
</StackPanel>
</ui:Card>
</StackPanel>
</ScrollViewer>
<!-- Bottom buttons -->
<Border Grid.Row="2"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0"
Padding="24,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Änderungen werden direkt jespeichert"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
<ui:Button Grid.Column="1"
Content="Zumachen"
Height="36"
Padding="24,0"
Click="Close_Click"
Appearance="Primary"/>
</Grid>
</Border>
</Grid>
</ui:FluentWindow>

View File

@@ -0,0 +1,231 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using MaxSlurper.Models;
using MaxSlurper.Services;
using Wpf.Ui.Controls;
namespace MaxSlurper.Controls
{
public partial class SettingsWindow : FluentWindow, INotifyPropertyChanged
{
private readonly AppSettings _settings;
private readonly ISettingsService _settingsService;
private readonly Action<AppSettings> _onSettingsChanged;
private bool _isCapturingHotkey = false;
public event PropertyChangedEventHandler? PropertyChanged;
private string _currentHotkey = "";
public string CurrentHotkey
{
get => _currentHotkey;
set
{
_currentHotkey = value;
OnPropertyChanged();
}
}
public SettingsWindow(AppSettings settings, ISettingsService settingsService, Action<AppSettings> onSettingsChanged)
{
InitializeComponent();
DataContext = this;
_settings = settings;
_settingsService = settingsService;
_onSettingsChanged = onSettingsChanged;
UpdateHotkeyDisplay();
}
private void UpdateHotkeyDisplay()
{
CurrentHotkey = _settings.HotkeyDisplayText;
}
private void ChangeHotkey_Click(object sender, RoutedEventArgs e)
{
if (_isCapturingHotkey)
return;
_isCapturingHotkey = true;
CurrentHotkey = "Drück deine Tasten...";
var captureWindow = new HotkeyCapture();
captureWindow.Owner = this;
if (captureWindow.ShowDialog() == true)
{
_settings.HotkeyModifiers = captureWindow.CapturedModifiers;
_settings.HotkeyKey = captureWindow.CapturedKey;
UpdateHotkeyDisplay();
SaveSettings();
}
else
{
UpdateHotkeyDisplay();
}
_isCapturingHotkey = false;
}
private void SaveSettings()
{
_settingsService.SaveSettings(_settings);
_onSettingsChanged(_settings);
}
private void Close_Click(object sender, RoutedEventArgs e)
{
Close();
}
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
// Hotkey capture dialog
public partial class HotkeyCapture : FluentWindow
{
public ModifierKeys CapturedModifiers { get; private set; } = ModifierKeys.None;
public Key CapturedKey { get; private set; } = Key.None;
public HotkeyCapture()
{
Title = "Tastenkombination aufnehmen";
Width = 400;
Height = 200;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
ExtendsContentIntoTitleBar = true;
WindowBackdropType = WindowBackdropType.Mica;
WindowCornerPreference = WindowCornerPreference.Round;
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var titleBar = new TitleBar
{
Title = "Tastenkombination",
ShowMaximize = false,
ShowMinimize = false
};
Grid.SetRow(titleBar, 0);
grid.Children.Add(titleBar);
var stackPanel = new StackPanel
{
Margin = new Thickness(24),
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center
};
Grid.SetRow(stackPanel, 1);
var infoText = new System.Windows.Controls.TextBlock
{
Text = "Drück die Tastenkombination, die de haben willst",
FontSize = 14,
TextAlignment = TextAlignment.Center,
Foreground = (System.Windows.Media.Brush)FindResource("TextFillColorSecondaryBrush"),
Margin = new Thickness(0, 0, 0, 16),
TextWrapping = TextWrapping.Wrap
};
stackPanel.Children.Add(infoText);
var keyDisplay = new System.Windows.Controls.TextBlock
{
Text = "Warte uff Eingabe...",
FontSize = 18,
FontWeight = FontWeights.SemiBold,
TextAlignment = TextAlignment.Center,
Foreground = (System.Windows.Media.Brush)FindResource("TextFillColorPrimaryBrush")
};
stackPanel.Children.Add(keyDisplay);
grid.Children.Add(stackPanel);
var buttonPanel = new StackPanel
{
Orientation = System.Windows.Controls.Orientation.Horizontal,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
Margin = new Thickness(24, 16, 24, 24)
};
Grid.SetRow(buttonPanel, 2);
var cancelButton = new Wpf.Ui.Controls.Button
{
Content = "Abbrechen",
Appearance = ControlAppearance.Secondary,
Margin = new Thickness(0, 0, 8, 0),
Padding = new Thickness(16, 8, 16, 8)
};
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
buttonPanel.Children.Add(cancelButton);
var okButton = new Wpf.Ui.Controls.Button
{
Content = "OK",
Appearance = ControlAppearance.Primary,
Padding = new Thickness(24, 8, 24, 8),
IsEnabled = false
};
okButton.Click += (s, e) => { DialogResult = true; Close(); };
buttonPanel.Children.Add(okButton);
grid.Children.Add(buttonPanel);
Content = grid;
// Capture keys
PreviewKeyDown += (s, e) =>
{
e.Handled = true;
var modifiers = Keyboard.Modifiers;
var key = e.Key == Key.System ? e.SystemKey : e.Key;
// Ignore modifier-only keys
if (key == Key.LeftCtrl || key == Key.RightCtrl ||
key == Key.LeftAlt || key == Key.RightAlt ||
key == Key.LeftShift || key == Key.RightShift ||
key == Key.LWin || key == Key.RWin)
{
return;
}
// Require at least one modifier
if (modifiers == ModifierKeys.None)
{
keyDisplay.Text = "Mindestens ein Modifier (Strg, Alt, Shift, Win) musste drücken!";
okButton.IsEnabled = false;
return;
}
CapturedModifiers = modifiers;
CapturedKey = key;
var parts = new System.Collections.Generic.List<string>();
if (modifiers.HasFlag(ModifierKeys.Control))
parts.Add("Strg");
if (modifiers.HasFlag(ModifierKeys.Alt))
parts.Add("Alt");
if (modifiers.HasFlag(ModifierKeys.Shift))
parts.Add("Shift");
if (modifiers.HasFlag(ModifierKeys.Windows))
parts.Add("Win");
parts.Add(key.ToString());
keyDisplay.Text = string.Join(" + ", parts);
okButton.IsEnabled = true;
};
}
}
}

180
MaxSlurper/MainWindow.xaml Normal file
View File

@@ -0,0 +1,180 @@
<ui:FluentWindow x:Class="MaxSlurper.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="MaxSlurper"
Height="600"
Width="450"
ResizeMode="NoResize"
WindowBackdropType="Mica"
ExtendsContentIntoTitleBar="True"
WindowCornerPreference="Round"
WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/logo.ico">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Title bar with window controls -->
<ui:TitleBar Grid.Row="0"
Title="MaxSlurper"
ShowMaximize="False"
ShowMinimize="True">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="pack://application:,,,/logo.png" />
</ui:TitleBar.Icon>
<ui:TitleBar.Header>
<ui:Button Appearance="Secondary"
Icon="{ui:SymbolIcon Settings24}"
ToolTip="Einstellungen"
Command="{Binding OpenSettingsCommand}"
Padding="8"
Margin="0,0,8,0"/>
</ui:TitleBar.Header>
</ui:TitleBar>
<!-- Main content -->
<Grid Grid.Row="1" Margin="16,8,16,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header: color preview + actions -->
<ui:Card Grid.Row="0" Margin="0,0,0,16" Padding="16">
<DockPanel LastChildFill="False">
<!-- Color preview -->
<Border Width="72"
Height="72"
Background="{Binding SelectedColorBrush}"
CornerRadius="12"
DockPanel.Dock="Left"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
Cursor="Hand"
ToolTip="Drück ma druff, dann kannste dir 'ne Farbe raussuche">
<Border.Effect>
<DropShadowEffect BlurRadius="8"
ShadowDepth="2"
Opacity="0.15"
Color="{Binding SelectedColorBrush.Color}" />
</Border.Effect>
<Border.InputBindings>
<MouseBinding MouseAction="LeftClick"
Command="{Binding OpenColorPickerCommand}" />
</Border.InputBindings>
</Border>
<!-- Details -->
<StackPanel Orientation="Vertical"
Margin="16,0,16,0"
VerticalAlignment="Center">
<TextBlock Text="Zuletzt jeklaut"
Style="{StaticResource SubtleText}"
Margin="0,0,0,4" />
<TextBlock Text="{Binding SelectedHex}"
FontWeight="SemiBold"
FontSize="20"
Margin="0,0,0,4"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="{Binding SelectedRgb}"
Style="{StaticResource SubtleText}" />
</StackPanel>
<!-- Actions -->
<StackPanel Orientation="Vertical"
DockPanel.Dock="Right">
<ui:Button Content="Farbe schlürfen"
Appearance="Primary"
Icon="{ui:SymbolIcon Eyedropper24}"
Command="{Binding PickColorCommand}"
Padding="16,8"
Width="160" />
<ui:Button Content="HEX kopieren"
Appearance="Secondary"
Icon="{ui:SymbolIcon Copy24}"
Command="{Binding CopyHexCommand}"
Padding="16,8"
Width="160"
Margin="0,8,0,0" />
</StackPanel>
</DockPanel>
</ui:Card>
<!-- History list -->
<ui:Card Grid.Row="1" Margin="0,0,0,0" Padding="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock Text="Wat ick schon so jeklaut hab"
Style="{StaticResource HeaderText}" />
<TextBlock Text="Druff klicken und ab dafuer, wa"
Style="{StaticResource SubtleText}"
Margin="0,4,0,0" />
</StackPanel>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding History}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:CardControl Margin="0,0,0,8"
Padding="8"
Cursor="Hand"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<ui:CardControl.InputBindings>
<MouseBinding MouseAction="LeftClick"
Command="{Binding DataContext.CopyHexCommand, RelativeSource={RelativeSource AncestorType=ui:FluentWindow}}"
CommandParameter="{Binding}" />
</ui:CardControl.InputBindings>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Color preview on the left -->
<Border Width="48"
Height="48"
Background="{Binding Brush}"
CornerRadius="8"
BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
BorderThickness="1"
VerticalAlignment="Center"
HorizontalAlignment="Left" />
<!-- Text info in the middle -->
<StackPanel Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center"
HorizontalAlignment="Left">
<TextBlock Text="{Binding Hex}"
FontWeight="Medium"
FontSize="15"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="{Binding Rgb}"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,2,0,0" />
</StackPanel>
</Grid>
</ui:CardControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</ui:Card>
</Grid>
</Grid>
</ui:FluentWindow>

View File

@@ -0,0 +1,111 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows;
using System.Windows.Forms;
using MaxSlurper.Services;
using MaxSlurper.Audio;
using MaxSlurper.ViewModels;
using Wpf.Ui.Controls;
using Wpf.Ui.Appearance;
namespace MaxSlurper
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : FluentWindow
{
private NotifyIcon _notifyIcon;
private readonly MainViewModel _vm;
private readonly IHotkeyService _hotkeyService;
public MainWindow()
{
InitializeComponent();
// Apply system theme and enable automatic theme switching
SystemThemeWatcher.Watch(this);
var colorPicker = new Win32ColorPickerService();
var audio = new SimpleAudioPlayer();
var settingsService = new SettingsService();
_hotkeyService = new HotkeyService();
_vm = new MainViewModel(colorPicker, audio, settingsService);
DataContext = _vm;
// Handle settings changes to update hotkey
_vm.OnSettingsChanged = (settings) =>
{
RegisterHotkey(settings);
};
SetupTrayIcon();
// Register initial hotkey after window is loaded
Loaded += (s, e) =>
{
RegisterHotkey(_vm.Settings);
};
}
private void RegisterHotkey(Models.AppSettings settings)
{
try
{
_hotkeyService.RegisterHotkey(
settings.HotkeyModifiers,
settings.HotkeyKey,
() =>
{
// Execute on UI thread
Dispatcher.BeginInvoke(new Action(() => _vm.PickColorCommand.Execute(null)));
});
}
catch
{
// Hotkey registration failed (might be already in use)
}
}
private void SetupTrayIcon()
{
_notifyIcon = new NotifyIcon();
_notifyIcon.Icon = System.Drawing.Icon.ExtractAssociatedIcon(System.Reflection.Assembly.GetEntryAssembly().Location);
_notifyIcon.Visible = true;
_notifyIcon.Text = "MaxSlurper";
var contextMenu = new ContextMenuStrip();
var pickItem = new ToolStripMenuItem("Farbe schlürfen");
// Use BeginInvoke to avoid synchronous invocation while windows may be closing
pickItem.Click += (s, e) => { Dispatcher.BeginInvoke(new Action(() => _vm.PickColorCommand.Execute(null))); };
var openItem = new ToolStripMenuItem("Fenster uffmachen");
openItem.Click += (s, e) => { Dispatcher.BeginInvoke(new Action(() => { Show(); WindowState = WindowState.Normal; Activate(); })); };
var settingsItem = new ToolStripMenuItem("Einstellungen");
settingsItem.Click += (s, e) => { Dispatcher.BeginInvoke(new Action(() => { Show(); WindowState = WindowState.Normal; Activate(); _vm.OpenSettingsCommand.Execute(null); })); };
var exitItem = new ToolStripMenuItem("Tschüss");
exitItem.Click += (s, e) => { _notifyIcon.Visible = false; System.Windows.Application.Current.Shutdown(); };
contextMenu.Items.Add(pickItem);
contextMenu.Items.Add(openItem);
contextMenu.Items.Add(settingsItem);
contextMenu.Items.Add(new ToolStripSeparator());
contextMenu.Items.Add(exitItem);
_notifyIcon.ContextMenuStrip = contextMenu;
_notifyIcon.DoubleClick += (s, e) => { Dispatcher.BeginInvoke(new Action(() => { Show(); WindowState = WindowState.Normal; Activate(); })); };
}
protected override void OnClosing(CancelEventArgs e)
{
e.Cancel = true;
Hide();
base.OnClosing(e);
}
protected override void OnClosed(EventArgs e)
{
_hotkeyService.UnregisterHotkey();
base.OnClosed(e);
}
}
}

View File

@@ -0,0 +1,53 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Authors>Mathias Wagner</Authors>
<Company>MaxSlurper</Company>
<Product>MaxSlurper</Product>
<Description>Weil Max danach gefragt hat</Description>
<Copyright>Copyright © 2025</Copyright>
<ApplicationIcon>logo.ico</ApplicationIcon>
</PropertyGroup>
<!-- Publishing settings for single-file deployment -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>embedded</DebugType>
<DebugSymbols>true</DebugSymbols>
<PublishTrimmed>false</PublishTrimmed>
<TrimMode>none</TrimMode>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
</PropertyGroup>
<ItemGroup>
<!-- ModernWpf removed to revert to standard WPF resources -->
</ItemGroup>
<!-- Include the provided Slurp.wav (located at the solution root) and copy it to output so it can be played at runtime -->
<ItemGroup>
<Content Include="..\Slurp.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<!-- Include logo files -->
<ItemGroup>
<Resource Include="logo.ico" />
<Resource Include="logo.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WPF-UI" Version="3.0.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,59 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace MaxSlurper.Models
{
public class AppSettings : INotifyPropertyChanged
{
private ModifierKeys _hotkeyModifiers = ModifierKeys.Control | ModifierKeys.Shift;
private Key _hotkeyKey = Key.C;
public ModifierKeys HotkeyModifiers
{
get => _hotkeyModifiers;
set
{
_hotkeyModifiers = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HotkeyDisplayText));
}
}
public Key HotkeyKey
{
get => _hotkeyKey;
set
{
_hotkeyKey = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HotkeyDisplayText));
}
}
public string HotkeyDisplayText
{
get
{
var parts = new System.Collections.Generic.List<string>();
if (HotkeyModifiers.HasFlag(ModifierKeys.Control))
parts.Add("Strg");
if (HotkeyModifiers.HasFlag(ModifierKeys.Alt))
parts.Add("Alt");
if (HotkeyModifiers.HasFlag(ModifierKeys.Shift))
parts.Add("Shift");
if (HotkeyModifiers.HasFlag(ModifierKeys.Windows))
parts.Add("Win");
parts.Add(HotkeyKey.ToString());
return string.Join(" + ", parts);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

View File

@@ -0,0 +1,21 @@
using System.Windows.Media;
namespace MaxSlurper.Models
{
public class ColorItem
{
public byte R { get; set; }
public byte G { get; set; }
public byte B { get; set; }
public string Hex { get; set; } = "#FFFFFF";
public string Rgb { get; set; } = "255,255,255";
public SolidColorBrush Brush { get; set; } = new SolidColorBrush(Colors.White);
public void UpdateDerived()
{
Hex = $"#{R:X2}{G:X2}{B:X2}";
Rgb = $"{R},{G},{B}";
Brush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(R, G, B));
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Windows.Input;
namespace MaxSlurper
{
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
namespace MaxSlurper.Services
{
public interface IHotkeyService
{
bool RegisterHotkey(ModifierKeys modifiers, Key key, Action callback);
void UnregisterHotkey();
}
public class HotkeyService : IHotkeyService
{
private const int WM_HOTKEY = 0x0312;
private const int HOTKEY_ID = 9000;
[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);
private IntPtr _windowHandle;
private HwndSource? _source;
private Action? _callback;
public bool RegisterHotkey(ModifierKeys modifiers, Key key, Action callback)
{
UnregisterHotkey();
_callback = callback;
// Get the main window handle
var mainWindow = System.Windows.Application.Current.MainWindow;
if (mainWindow == null)
return false;
_windowHandle = new WindowInteropHelper(mainWindow).Handle;
_source = HwndSource.FromHwnd(_windowHandle);
if (_source == null)
return false;
_source.AddHook(HwndHook);
uint fsModifiers = 0;
if (modifiers.HasFlag(ModifierKeys.Alt))
fsModifiers |= 0x0001; // MOD_ALT
if (modifiers.HasFlag(ModifierKeys.Control))
fsModifiers |= 0x0002; // MOD_CONTROL
if (modifiers.HasFlag(ModifierKeys.Shift))
fsModifiers |= 0x0004; // MOD_SHIFT
if (modifiers.HasFlag(ModifierKeys.Windows))
fsModifiers |= 0x0008; // MOD_WIN
uint vk = (uint)KeyInterop.VirtualKeyFromKey(key);
return RegisterHotKey(_windowHandle, HOTKEY_ID, fsModifiers, vk);
}
public void UnregisterHotkey()
{
if (_source != null)
{
_source.RemoveHook(HwndHook);
_source = null;
}
if (_windowHandle != IntPtr.Zero)
{
UnregisterHotKey(_windowHandle, HOTKEY_ID);
_windowHandle = IntPtr.Zero;
}
_callback = null;
}
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_HOTKEY && wParam.ToInt32() == HOTKEY_ID)
{
_callback?.Invoke();
handled = true;
}
return IntPtr.Zero;
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using System.Windows.Media;
namespace MaxSlurper.Services
{
public interface IColorPickerService
{
System.Threading.Tasks.Task<System.Windows.Media.Color?> PickColorAsync();
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.IO;
using System.Text.Json;
using System.Windows.Input;
using MaxSlurper.Models;
namespace MaxSlurper.Services
{
public interface ISettingsService
{
AppSettings LoadSettings();
void SaveSettings(AppSettings settings);
}
public class SettingsService : ISettingsService
{
private readonly string _settingsPath;
public SettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var appFolder = Path.Combine(appData, "MaxSlurper");
Directory.CreateDirectory(appFolder);
_settingsPath = Path.Combine(appFolder, "settings.json");
}
public AppSettings LoadSettings()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
var data = JsonSerializer.Deserialize<SettingsData>(json);
if (data != null)
{
var settings = new AppSettings();
// Parse modifiers
ModifierKeys modifiers = ModifierKeys.None;
if (data.HotkeyModifiers != null)
{
if (data.HotkeyModifiers.Contains("Control"))
modifiers |= ModifierKeys.Control;
if (data.HotkeyModifiers.Contains("Alt"))
modifiers |= ModifierKeys.Alt;
if (data.HotkeyModifiers.Contains("Shift"))
modifiers |= ModifierKeys.Shift;
if (data.HotkeyModifiers.Contains("Windows"))
modifiers |= ModifierKeys.Windows;
}
settings.HotkeyModifiers = modifiers;
// Parse key
if (!string.IsNullOrEmpty(data.HotkeyKey) &&
Enum.TryParse<Key>(data.HotkeyKey, out var key))
{
settings.HotkeyKey = key;
}
return settings;
}
}
}
catch { }
return new AppSettings();
}
public void SaveSettings(AppSettings settings)
{
try
{
var modifiers = new System.Collections.Generic.List<string>();
if (settings.HotkeyModifiers.HasFlag(ModifierKeys.Control))
modifiers.Add("Control");
if (settings.HotkeyModifiers.HasFlag(ModifierKeys.Alt))
modifiers.Add("Alt");
if (settings.HotkeyModifiers.HasFlag(ModifierKeys.Shift))
modifiers.Add("Shift");
if (settings.HotkeyModifiers.HasFlag(ModifierKeys.Windows))
modifiers.Add("Windows");
var data = new SettingsData
{
HotkeyModifiers = modifiers.ToArray(),
HotkeyKey = settings.HotkeyKey.ToString()
};
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_settingsPath, json);
}
catch { }
}
private class SettingsData
{
public string[]? HotkeyModifiers { get; set; }
public string? HotkeyKey { get; set; }
}
}
}

View File

@@ -0,0 +1,283 @@
using System.Threading.Tasks;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System;
using System.Windows.Media;
using System.Windows.Threading;
using SD = System.Drawing;
namespace MaxSlurper.Services
{
public class Win32ColorPickerService : IColorPickerService
{
[DllImport("user32.dll")]
private static extern System.IntPtr GetDC(System.IntPtr hwnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(System.IntPtr hwnd, System.IntPtr hdc);
[DllImport("gdi32.dll")]
private static extern uint GetPixel(System.IntPtr hdc, int nXPos, int nYPos);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
public async Task<System.Windows.Media.Color?> PickColorAsync()
{
return await Task.Run(() =>
{
var tcs = new TaskCompletionSource<System.Windows.Media.Color?>();
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
// If app is shutting down, abort
if (System.Windows.Application.Current == null || System.Windows.Application.Current.Dispatcher.HasShutdownStarted || System.Windows.Application.Current.Dispatcher.HasShutdownFinished)
{
tcs.TrySetResult(null);
return;
}
bool finished = false; // guard to ensure cleanup runs once
void Finish(System.Windows.Media.Color? result)
{
if (finished) return;
finished = true;
try { tcs.TrySetResult(result); } catch { }
}
// Create full-screen transparent overlay covering virtual screen (multi-monitor aware)
var overlay = new System.Windows.Window
{
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
// near-transparent background so the window is hit-testable but visually transparent
Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1,0,0,0)),
Topmost = true,
Left = SystemParameters.VirtualScreenLeft,
Top = SystemParameters.VirtualScreenTop,
Width = SystemParameters.VirtualScreenWidth,
Height = SystemParameters.VirtualScreenHeight,
ShowInTaskbar = false,
ShowActivated = true
};
// Magnifier settings - 17x17 grid for clearer center view
const int sampleSize = 17; // pixels captured (square)
const int magnifierSize = 238; // display size in px (17 * 14 = 238 for even pixel sizing)
var root = new System.Windows.Controls.Canvas();
overlay.Content = root;
// Image that shows the magnified (pixelated) sample
var magnifierImage = new System.Windows.Controls.Image
{
Width = magnifierSize,
Height = magnifierSize,
Stretch = Stretch.Fill,
RenderTransformOrigin = new System.Windows.Point(0.5,0.5),
ClipToBounds = true
};
RenderOptions.SetBitmapScalingMode(magnifierImage, BitmapScalingMode.NearestNeighbor);
// Clip the image to rounded corners
var imageClip = new RectangleGeometry
{
Rect = new Rect(0, 0, magnifierSize, magnifierSize),
RadiusX = 12,
RadiusY = 12
};
magnifierImage.Clip = imageClip;
// Outer border with modern, rounded styling
var outerBorder = new Border
{
Width = magnifierSize + 16,
Height = magnifierSize + 16,
BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 60, 60, 65)),
BorderThickness = new Thickness(4),
CornerRadius = new CornerRadius(16),
Background = new SolidColorBrush(System.Windows.Media.Color.FromArgb(240, 20, 20, 24)),
Padding = new Thickness(4),
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 30,
ShadowDepth = 4,
Opacity = 0.6,
Color = Colors.Black
},
Child = new Grid()
};
// Inner container for the magnified image
var innerBorder = new Border
{
Width = magnifierSize,
Height = magnifierSize,
CornerRadius = new CornerRadius(12),
ClipToBounds = true,
Child = new Grid()
};
((Grid)innerBorder.Child).Children.Add(magnifierImage);
// Center pixel indicator - a rounded border around the center pixel
const double pixelSize = magnifierSize / (double)sampleSize; // size of each pixel in the magnified view
var centerIndicator = new Border
{
Width = pixelSize + 4,
Height = pixelSize + 4,
BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 255, 255, 255)),
BorderThickness = new Thickness(3),
CornerRadius = new CornerRadius(4),
IsHitTestVisible = false,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
VerticalAlignment = System.Windows.VerticalAlignment.Center,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 4,
ShadowDepth = 0,
Opacity = 0.8,
Color = Colors.White
}
};
// Inner shadow border for depth
var centerInnerBorder = new Border
{
Width = pixelSize,
Height = pixelSize,
BorderBrush = new SolidColorBrush(System.Windows.Media.Color.FromArgb(200, 0, 0, 0)),
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(2),
IsHitTestVisible = false,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
VerticalAlignment = System.Windows.VerticalAlignment.Center
};
((Grid)innerBorder.Child).Children.Add(centerIndicator);
((Grid)innerBorder.Child).Children.Add(centerInnerBorder);
((Grid)outerBorder.Child).Children.Add(innerBorder);
root.Children.Add(outerBorder);
void UpdateMagnifier()
{
var pt = System.Windows.Forms.Control.MousePosition;
int cx = pt.X;
int cy = pt.Y;
double left = cx +20;
double top = cy +20;
if (left + outerBorder.Width > SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth)
left = cx - outerBorder.Width -20;
if (top + outerBorder.Height > SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight)
top = cy - outerBorder.Height -20;
Canvas.SetLeft(outerBorder, left - SystemParameters.VirtualScreenLeft);
Canvas.SetTop(outerBorder, top - SystemParameters.VirtualScreenTop);
var sampleRectX = cx - (sampleSize /2);
var sampleRectY = cy - (sampleSize /2);
try
{
using (var bmp = new SD.Bitmap(sampleSize, sampleSize, SD.Imaging.PixelFormat.Format32bppArgb))
{
using (var g = SD.Graphics.FromImage(bmp))
{
g.CopyFromScreen(sampleRectX, sampleRectY,0,0, new SD.Size(sampleSize, sampleSize), SD.CopyPixelOperation.SourceCopy);
}
var hBitmap = bmp.GetHbitmap();
try
{
var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(sampleSize, sampleSize));
magnifierImage.Source = bitmapSource;
}
finally
{
DeleteObject(hBitmap);
}
}
}
catch { }
}
var timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(30) };
timer.Tick += (s, e) => UpdateMagnifier();
// Mouse click: pick color
overlay.MouseLeftButtonDown += (s, e) =>
{
if (finished) return;
var pt = System.Windows.Forms.Control.MousePosition;
var hdc = GetDC(IntPtr.Zero);
try
{
var pixel = GetPixel(hdc, pt.X, pt.Y);
byte r = (byte)(pixel &0x000000FF);
byte g = (byte)((pixel &0x0000FF00) >>8);
byte b = (byte)((pixel &0x00FF0000) >>16);
var color = System.Windows.Media.Color.FromRgb(r, g, b);
Finish(color);
}
finally
{
try { ReleaseDC(IntPtr.Zero, hdc); } catch { }
try { timer.Stop(); } catch { }
try { System.Windows.Input.Mouse.Capture(null); } catch { }
try { overlay.Close(); } catch (InvalidOperationException) { /* ignore if closing */ }
}
};
overlay.KeyDown += (s, e) =>
{
if (e.Key == System.Windows.Input.Key.Escape)
{
Finish(null);
try { timer.Stop(); } catch { }
try { System.Windows.Input.Mouse.Capture(null); } catch { }
try { overlay.Close(); } catch (InvalidOperationException) { }
}
};
overlay.Closed += (s, e) =>
{
// ensure cleanup
Finish(null);
try { timer.Stop(); } catch { }
try { System.Windows.Input.Mouse.Capture(null); } catch { }
};
// Start timer and show overlay only if application is running
timer.Start();
try
{
overlay.Show();
}
catch (InvalidOperationException)
{
// host window might be closing; abort
Finish(null);
try { timer.Stop(); } catch { }
try { System.Windows.Input.Mouse.Capture(null); } catch { }
try { overlay.Close(); } catch { }
return;
}
try { overlay.Activate(); } catch { }
try { overlay.Focus(); } catch { }
try { System.Windows.Input.Mouse.Capture(overlay, System.Windows.Input.CaptureMode.SubTree); } catch { }
// initial update
UpdateMagnifier();
});
return tcs.Task.Result;
});
}
}
}

View File

View File

@@ -0,0 +1,148 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Windows.Media;
using MaxSlurper.Models;
using MaxSlurper.Services;
using MaxSlurper.Audio;
using MaxSlurper.Controls;
namespace MaxSlurper.ViewModels
{
public class MainViewModel : INotifyPropertyChanged
{
private readonly IColorPickerService _colorPicker;
private readonly IAudioPlayer _audioPlayer;
private readonly ISettingsService _settingsService;
public ObservableCollection<ColorItem> History { get; } = new ObservableCollection<ColorItem>();
public AppSettings Settings { get; private set; }
private ColorItem? _selected;
public ColorItem? Selected
{
get => _selected;
set { _selected = value; OnPropertyChanged(); UpdateSelected(); ((RelayCommand)CopyHexCommand).RaiseCanExecuteChanged(); }
}
private System.Windows.Media.Brush _selectedBrush = System.Windows.Media.Brushes.White;
public System.Windows.Media.Brush SelectedColorBrush { get => _selectedBrush; set { _selectedBrush = value; OnPropertyChanged(); } }
private string _selectedHex = "#FFFFFF";
public string SelectedHex { get => _selectedHex; set { _selectedHex = value; OnPropertyChanged(); } }
private string _selectedRgb = "255,255,255";
public string SelectedRgb { get => _selectedRgb; set { _selectedRgb = value; OnPropertyChanged(); } }
public ICommand PickColorCommand { get; }
public ICommand ClearHistoryCommand { get; }
public ICommand CopyHexCommand { get; }
public ICommand OpenColorPickerCommand { get; }
public ICommand OpenSettingsCommand { get; }
public MainViewModel(IColorPickerService colorPicker, IAudioPlayer audioPlayer, ISettingsService settingsService)
{
_colorPicker = colorPicker;
_audioPlayer = audioPlayer;
_settingsService = settingsService;
Settings = _settingsService.LoadSettings();
PickColorCommand = new RelayCommand(async _ => await PickColor());
ClearHistoryCommand = new RelayCommand(_ => ClearHistory());
CopyHexCommand = new RelayCommand(CopyHex, _ => Selected != null || _ is ColorItem);
OpenColorPickerCommand = new RelayCommand(_ => OpenColorPicker());
OpenSettingsCommand = new RelayCommand(_ => OpenSettings());
}
public System.Action<AppSettings>? OnSettingsChanged { get; set; }
private async System.Threading.Tasks.Task PickColor()
{
var color = await _colorPicker.PickColorAsync();
if (color == null) return;
// play the provided Slurp.wav which is copied to the application's output folder
_audioPlayer.Play("Slurp.wav");
var item = new ColorItem
{
R = color.Value.R,
G = color.Value.G,
B = color.Value.B,
};
item.UpdateDerived();
History.Insert(0, item);
Selected = item;
System.Windows.Clipboard.SetText(item.Hex);
}
private void OpenColorPicker()
{
var currentColor = Selected != null
? System.Windows.Media.Color.FromRgb(Selected.R, Selected.G, Selected.B)
: Colors.Red;
var picker = new ColorPickerWindow(currentColor);
if (picker.ShowDialog() == true)
{
var color = picker.SelectedColor;
var item = new ColorItem
{
R = color.R,
G = color.G,
B = color.B,
};
item.UpdateDerived();
History.Insert(0, item);
Selected = item;
System.Windows.Clipboard.SetText(item.Hex);
}
}
private void OpenSettings()
{
var settingsWindow = new SettingsWindow(Settings, _settingsService, (settings) =>
{
OnSettingsChanged?.Invoke(settings);
});
settingsWindow.ShowDialog();
}
private void ClearHistory()
{
History.Clear();
}
private void CopyHex(object? parameter)
{
var item = parameter as ColorItem ?? Selected;
if (item == null) return;
// Update selected to show which color was clicked
Selected = item;
System.Windows.Clipboard.SetText(item.Hex);
// Show a notification (optional - can add UI feedback)
}
private void UpdateSelected()
{
if (Selected == null) return;
SelectedColorBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(Selected.R, Selected.G, Selected.B));
SelectedHex = Selected.Hex;
SelectedRgb = Selected.Rgb;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

BIN
MaxSlurper/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
MaxSlurper/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

BIN
Slurp.wav Normal file

Binary file not shown.

130
build.ps1 Normal file
View File

@@ -0,0 +1,130 @@
# MaxSlurper Build and Package Script
# This script builds the MaxSlurper application as a standalone executable
param(
[string]$Version = "1.0.0",
[switch]$SkipBuild = $false,
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$StartTime = Get-Date
# Color output functions
function Write-Info { Write-Host $args -ForegroundColor Cyan }
function Write-Success { Write-Host $args -ForegroundColor Green }
function Write-Error { Write-Host $args -ForegroundColor Red }
function Write-Warning { Write-Host $args -ForegroundColor Yellow }
# Script variables
$ScriptRoot = $PSScriptRoot
$ProjectName = "MaxSlurper"
$ProjectDir = Join-Path $ScriptRoot $ProjectName
$ProjectFile = Join-Path $ProjectDir "$ProjectName.csproj"
$PublishDir = Join-Path $ScriptRoot "publish"
$ReleaseDir = Join-Path $ScriptRoot "release"
Write-Info "======================================"
Write-Info " MaxSlurper Build Script"
Write-Info "======================================"
Write-Info "Version: $Version"
Write-Info "Configuration: $Configuration"
Write-Info ""
# Check if project file exists
if (-not (Test-Path $ProjectFile)) {
Write-Error "Project file not found: $ProjectFile"
exit 1
}
# Clean previous builds
Write-Info "Cleaning previous builds..."
if (Test-Path $PublishDir) { Remove-Item $PublishDir -Recurse -Force }
if (Test-Path $ReleaseDir) { Remove-Item $ReleaseDir -Recurse -Force }
New-Item -ItemType Directory -Path $PublishDir -Force | Out-Null
New-Item -ItemType Directory -Path $ReleaseDir -Force | Out-Null
Write-Success "? Cleanup complete"
# Build the application
if (-not $SkipBuild) {
Write-Info ""
Write-Info "Building $ProjectName..."
# Restore dependencies
Write-Info "Restoring NuGet packages..."
dotnet restore $ProjectFile
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to restore packages"
exit 1
}
# Publish the application
Write-Info "Publishing application..."
dotnet publish $ProjectFile `
--configuration $Configuration `
--output $PublishDir `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:IncludeAllContentForSelfExtract=true `
-p:EnableCompressionInSingleFile=true `
-p:DebugType=embedded `
-p:Version=$Version
if ($LASTEXITCODE -ne 0) {
Write-Error "Build failed"
exit 1
}
Write-Success "? Build complete"
# Copy the executable to release folder
$ExePath = Join-Path $PublishDir "$ProjectName.exe"
if (Test-Path $ExePath) {
Copy-Item $ExePath -Destination (Join-Path $ReleaseDir "$ProjectName-$Version.exe")
Copy-Item $ExePath -Destination (Join-Path $ReleaseDir "$ProjectName.exe")
Write-Success "? Executable copied to release folder"
}
# Check if Slurp.wav was copied
$SlurpWav = Join-Path $PublishDir "Slurp.wav"
if (Test-Path $SlurpWav) {
Write-Success "? Slurp.wav included"
} else {
Write-Warning "? Slurp.wav not found - sound may not work"
}
} else {
Write-Warning "Skipping build (using existing files)"
}
# Summary
$BuildTime = (Get-Date) - $StartTime
Write-Info ""
Write-Info "======================================"
Write-Success " Build Complete!"
Write-Info "======================================"
Write-Info ""
Write-Info "Release files location: $ReleaseDir"
if (Test-Path (Join-Path $ReleaseDir "$ProjectName.exe")) {
Write-Success "? Executable: MaxSlurper.exe"
}
if (Test-Path (Join-Path $ReleaseDir "$ProjectName-$Version.exe")) {
Write-Success "? Versioned: MaxSlurper-$Version.exe"
}
Write-Info ""
Write-Info "Build time: $($BuildTime.ToString('mm\:ss'))"
Write-Info ""
# Open release folder
$OpenFolder = Read-Host "Open release folder? (Y/N)"
if ($OpenFolder -eq "Y" -or $OpenFolder -eq "y") {
Start-Process explorer.exe $ReleaseDir
}
Write-Success "Allet jut jemacht! ??"