Initial commit: EngineeringSync v1.0.0

Vollständige Implementierung des EngineeringSync-Middleware-Tools:
- Windows Service (Kestrel :5050) mit FileSystemWatcher + SignalR
- WPF Tray-App mit PendingChanges- und Projektverwaltungs-Fenster
- Setup-Wizard (8-Schritte-Installer)
- SQLite/EF Core Datenschicht (WAL-Modus)
- SHA-256-basiertes Debouncing (2s Fenster)
- Backup-System mit konfigurierbarer Aufbewahrung

Bugfixes & Verbesserungen:
- BUG-1: AppDbContext OnConfiguring invertierte Bedingung behoben
- BUG-2: Event-Handler-Leak in TrayApp (Fenster-Singleton-Pattern)
- BUG-3: ProjectConfigChanged SignalR-Signal in allen CRUD-Endpoints
- BUG-5: Rename-Sync löscht alte Datei im Simulations-Ordner
- BUG-6: Doppeltes Dispose von SignalR verhindert
- BUG-7: Registry-Deinstallation nur EngineeringSync-Eintrag entfernt
- S1: Path-Traversal-Schutz via SafeCombine() im SyncManager
- E1: FSW Buffer 64KB + automatischer Re-Scan bei Overflow
- E2: Retry-Logik (3x) für gesperrte Dateien mit exponentiellem Backoff
- E4: Channel.Writer.TryComplete() beim Shutdown
- C2: HubMethodNames-Konstanten statt Magic Strings
- E3: Pagination in Changes-API (page/pageSize Query-Parameter)
- A1: Fire-and-Forget mit try/catch + Logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
EngineeringSync
2026-03-26 21:52:26 +01:00
commit 04ae8a0aae
98 changed files with 8172 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
<Application x:Class="EngineeringSync.Setup.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:EngineeringSync.Setup.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/WizardStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<conv:BoolToVisibilityConverter x:Key="BoolToVisConverter"/>
<conv:BoolToInverseVisibilityConverter x:Key="BoolToInvVisConverter"/>
<conv:LastStepLabelConverter x:Key="LastStepLabelConverter"/>
<conv:StringToVisibilityConverter x:Key="StringToVisConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,82 @@
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Windows;
using Microsoft.Win32;
namespace EngineeringSync.Setup;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
if (e.Args.Length > 0 && e.Args[0].Equals("/uninstall", StringComparison.OrdinalIgnoreCase))
{
RunUninstall();
Shutdown();
return;
}
var window = new Views.WizardWindow();
window.Show();
}
private void RunUninstall()
{
var serviceName = "EngineeringSync";
try
{
var sc = new ServiceController(serviceName);
if (sc.Status != ServiceControllerStatus.Stopped)
{
sc.Stop();
sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10));
}
}
catch { }
try
{
Process.Start(new ProcessStartInfo
{
FileName = "sc",
Arguments = "delete EngineeringSync",
UseShellExecute = false,
CreateNoWindow = true
})?.WaitForExit();
}
catch { }
try
{
using var runKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
runKey?.DeleteValue("EngineeringSync.TrayApp", false);
}
catch { }
try
{
Registry.LocalMachine.DeleteSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\EngineeringSync", false);
}
catch { }
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
var linkPath = Path.Combine(desktop, "EngineeringSync.lnk");
if (File.Exists(linkPath))
File.Delete(linkPath);
var startMenu = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs", "EngineeringSync");
if (Directory.Exists(startMenu))
Directory.Delete(startMenu, true);
var msg = "EngineeringSync wurde erfolgreich deinstalliert.";
MessageBox.Show(msg, "Deinstallation", MessageBoxButton.OK, MessageBoxImage.Information);
}
}

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)
)]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,40 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace EngineeringSync.Setup.Converters;
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object v, Type t, object p, CultureInfo c) =>
v is true ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
throw new NotSupportedException();
}
public class BoolToInverseVisibilityConverter : IValueConverter
{
public object Convert(object v, Type t, object p, CultureInfo c) =>
v is true ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
throw new NotSupportedException();
}
/// <summary>
/// Gibt "Jetzt installieren" wenn true (letzter normaler Schritt), sonst "Weiter".
/// </summary>
public class LastStepLabelConverter : IValueConverter
{
public object Convert(object v, Type t, object p, CultureInfo c) =>
v is true ? "Jetzt installieren" : "Weiter";
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
throw new NotSupportedException();
}
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object v, Type t, object p, CultureInfo c) =>
string.IsNullOrEmpty(v as string) ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
throw new NotSupportedException();
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<AssemblyName>EngineeringSync.Setup</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\setup-icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\setup-icon.ico" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
<Window x:Class="EngineeringSync.Setup.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:EngineeringSync.Setup"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,23 @@
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace EngineeringSync.Setup;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,304 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using EngineeringSync.Setup.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.Setup.Services;
/// <summary>
/// Führt alle Installationsschritte aus. Gibt Fortschritt und Log-Meldungen
/// über Events zurück, damit die UI in Echtzeit aktualisiert werden kann.
/// </summary>
public class InstallerService(WizardState state)
{
public event Action<int, string>? Progress;
public event Action<string>? LogMessage;
private void Report(int percent, string step, string? log = null)
{
Progress?.Invoke(percent, step);
LogMessage?.Invoke(log ?? step);
}
/// <summary>Hauptinstallationsablauf läuft auf einem Background-Thread.</summary>
public async Task InstallAsync()
{
await Task.Run(async () =>
{
// 1. Installationsverzeichnis anlegen
Report(5, "Installationsverzeichnis anlegen...");
Directory.CreateDirectory(state.InstallPath);
await Delay();
// 2. Programmdateien kopieren
Report(15, "Programmdateien werden kopiert...");
await CopyApplicationFilesAsync();
// 3. Datenbank-Verzeichnis anlegen
Report(30, "Datenbankverzeichnis anlegen...");
var dbDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"EngineeringSync");
Directory.CreateDirectory(dbDir);
Report(33, "Datenbankverzeichnis angelegt.", $"Datenbank-Pfad: {dbDir}");
await Delay();
// 4. Erstkonfigurations-Datei schreiben
Report(40, "Erstkonfiguration wird gespeichert...");
WriteFirstRunConfig(dbDir);
await Delay();
// 5. Bestehenden Dienst stoppen & deinstallieren (Upgrade-Szenario)
Report(50, "Vorherige Installation wird geprüft...");
await StopAndRemoveExistingServiceAsync();
await Delay();
// 6. Windows Service registrieren
Report(60, "Windows-Dienst wird registriert...");
var serviceExe = Path.Combine(state.InstallPath, "EngineeringSync.Service.exe");
await RegisterWindowsServiceAsync(serviceExe);
await Delay();
// 7. Autostart für TrayApp
if (state.AutoStartTrayApp)
{
Report(70, "TrayApp-Autostart wird konfiguriert...");
ConfigureTrayAppAutostart();
await Delay();
}
// 8. Verknüpfungen erstellen
Report(78, "Verknüpfungen werden erstellt...");
if (state.CreateDesktopShortcut) CreateShortcut(ShortcutTarget.Desktop);
if (state.CreateStartMenuEntry) CreateShortcut(ShortcutTarget.StartMenu);
await Delay();
// 9. Add/Remove Programs Eintrag
Report(85, "Deinstallations-Eintrag wird angelegt...");
RegisterUninstallEntry();
await Delay();
// 10. Dienst starten
if (state.AutoStartService)
{
Report(92, "Windows-Dienst wird gestartet...");
await StartServiceAsync();
}
Report(100, "Installation abgeschlossen.", "✓ EngineeringSync wurde erfolgreich installiert.");
});
}
// ── Hilfsmethoden ─────────────────────────────────────────────────
private async Task CopyApplicationFilesAsync()
{
var sourceDir = Path.GetFullPath(AppContext.BaseDirectory).TrimEnd(Path.DirectorySeparatorChar);
var targetDir = Path.GetFullPath(state.InstallPath).TrimEnd(Path.DirectorySeparatorChar);
// Wenn das Setup bereits aus dem Zielverzeichnis läuft (z.B. nach Inno-Setup-Installation),
// wurden die Dateien bereits kopiert nichts zu tun.
if (string.Equals(sourceDir, targetDir, StringComparison.OrdinalIgnoreCase))
{
LogMessage?.Invoke(" Dateien bereits installiert (Inno-Setup) - Kopieren wird übersprungen.");
return;
}
var patterns = new[] { "*.exe", "*.dll", "*.json", "*.pdb" };
var allFiles = patterns
.SelectMany(p => Directory.GetFiles(sourceDir, p))
.Distinct()
.ToList();
for (int i = 0; i < allFiles.Count; i++)
{
var src = allFiles[i];
var dest = Path.Combine(targetDir, Path.GetFileName(src));
File.Copy(src, dest, overwrite: true);
LogMessage?.Invoke($" Kopiert: {Path.GetFileName(src)}");
await Task.Delay(30); // Kurze Pause für UI-Responsivität
}
}
private void WriteFirstRunConfig(string dbDir)
{
var config = new
{
FirstRun = new
{
ProjectName = state.ProjectName,
EngineeringPath = state.EngineeringPath,
SimulationPath = state.SimulationPath,
FileExtensions = state.WatchAllFiles ? "*" : state.FileExtensions,
WatchAllFiles = state.WatchAllFiles
},
Backup = new // NEU
{
BackupEnabled = state.BackupEnabled,
BackupPath = state.BackupUseCustomPath ? state.BackupCustomPath : (string?)null,
MaxBackupsPerFile = state.MaxBackupsPerFile
}
};
var json = JsonSerializer.Serialize(config,
new JsonSerializerOptions { WriteIndented = true });
var configPath = Path.Combine(state.InstallPath, "firstrun-config.json");
File.WriteAllText(configPath, json);
LogMessage?.Invoke($" Konfiguration geschrieben: {configPath}");
}
private async Task StopAndRemoveExistingServiceAsync()
{
try
{
var stopResult = await RunProcessAsync("sc", "stop EngineeringSync");
if (stopResult.exitCode == 0)
{
LogMessage?.Invoke(" Bestehender Dienst gestoppt.");
await Task.Delay(1500); // Warten bis Dienst wirklich gestoppt
}
await RunProcessAsync("sc", "delete EngineeringSync");
LogMessage?.Invoke(" Bestehender Dienst entfernt.");
}
catch { /* Kein vorheriger Dienst OK */ }
}
private async Task RegisterWindowsServiceAsync(string exePath)
{
if (!File.Exists(exePath))
{
LogMessage?.Invoke($" WARNUNG: Service-EXE nicht gefunden: {exePath}");
LogMessage?.Invoke(" (Im Entwicklungsmodus Service-Registrierung übersprungen)");
return;
}
var startType = state.AutoStartService ? "auto" : "demand";
var args = $"create EngineeringSync binPath= \"{exePath}\" " +
$"start= {startType} " +
$"DisplayName= \"EngineeringSync Watcher Service\"";
var (exitCode, output, error) = await RunProcessAsync("sc", args);
if (exitCode == 0)
LogMessage?.Invoke(" ✓ Windows-Dienst registriert.");
else
{
LogMessage?.Invoke($" FEHLER bei sc create (Code {exitCode}): {error}");
throw new InvalidOperationException($"Service-Registrierung fehlgeschlagen: {error}");
}
}
private void ConfigureTrayAppAutostart()
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
if (!File.Exists(trayExe))
{
LogMessage?.Invoke(" TrayApp nicht gefunden Autostart übersprungen.");
return;
}
using var key = Registry.CurrentUser.OpenSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", writable: true)!;
key.SetValue("EngineeringSync.TrayApp", $"\"{trayExe}\"");
LogMessage?.Invoke(" ✓ TrayApp-Autostart in Registry eingetragen.");
}
private enum ShortcutTarget { Desktop, StartMenu }
private void CreateShortcut(ShortcutTarget target)
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
var dir = target == ShortcutTarget.Desktop
? Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory)
: Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
"Programs", "EngineeringSync");
Directory.CreateDirectory(dir);
var linkPath = Path.Combine(dir, "EngineeringSync.lnk");
CreateWindowsShortcut(linkPath, trayExe, state.InstallPath, "EngineeringSync Tray App");
LogMessage?.Invoke($" ✓ Verknüpfung erstellt: {linkPath}");
}
private static void CreateWindowsShortcut(string linkPath, string targetPath,
string workingDir, string description)
{
// WScript.Shell COM-Interop für Verknüpfungs-Erstellung
var type = Type.GetTypeFromProgID("WScript.Shell")!;
dynamic shell = Activator.CreateInstance(type)!;
var shortcut = shell.CreateShortcut(linkPath);
shortcut.TargetPath = targetPath;
shortcut.WorkingDirectory = workingDir;
shortcut.Description = description;
shortcut.Save();
Marshal.FinalReleaseComObject(shortcut);
Marshal.FinalReleaseComObject(shell);
}
private void RegisterUninstallEntry()
{
using var key = Registry.LocalMachine.CreateSubKey(
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\EngineeringSync");
var uninstallExe = Path.Combine(state.InstallPath, "EngineeringSync.Setup.exe");
key.SetValue("DisplayName", "EngineeringSync");
key.SetValue("DisplayVersion", "1.0.0");
key.SetValue("Publisher", "EngineeringSync");
key.SetValue("InstallLocation", state.InstallPath);
key.SetValue("UninstallString", $"\"{uninstallExe}\" /uninstall");
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
key.SetValue("EstimatedSize", 45000, RegistryValueKind.DWord);
key.SetValue("DisplayIcon", Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe"));
LogMessage?.Invoke(" ✓ Deinstallations-Eintrag in Registry angelegt.");
}
private async Task StartServiceAsync()
{
var (exitCode, _, error) = await RunProcessAsync("sc", "start EngineeringSync");
if (exitCode == 0)
LogMessage?.Invoke(" ✓ Windows-Dienst gestartet.");
else
LogMessage?.Invoke($" WARNUNG: Dienst konnte nicht gestartet werden ({error})");
}
public void LaunchTrayApp()
{
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
if (File.Exists(trayExe))
Process.Start(new ProcessStartInfo(trayExe) { UseShellExecute = true });
}
private static async Task<(int exitCode, string output, string error)> RunProcessAsync(
string fileName, string arguments)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, output.Trim(), error.Trim());
}
private static Task Delay() => Task.Delay(400);
}

View File

@@ -0,0 +1,361 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ═══════════════════════════════════════════
FARBEN & BRUSHES
═══════════════════════════════════════════ -->
<Color x:Key="AccentColor">#0078D4</Color>
<Color x:Key="AccentHoverColor">#106EBE</Color>
<Color x:Key="AccentPressColor">#005A9E</Color>
<Color x:Key="SidebarTopColor">#1B1B2F</Color>
<Color x:Key="SidebarBottomColor">#16213E</Color>
<Color x:Key="SuccessColor">#107C10</Color>
<Color x:Key="ErrorColor">#C42B1C</Color>
<Color x:Key="WarningColor">#CA5010</Color>
<Color x:Key="SurfaceColor">#FAFAFA</Color>
<Color x:Key="BorderColor">#E0E0E0</Color>
<Color x:Key="TextPrimaryColor">#1A1A1A</Color>
<Color x:Key="TextSecondaryColor">#5F5F5F</Color>
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
<SolidColorBrush x:Key="AccentHoverBrush" Color="{StaticResource AccentHoverColor}"/>
<SolidColorBrush x:Key="AccentPressBrush" Color="{StaticResource AccentPressColor}"/>
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}"/>
<SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource ErrorColor}"/>
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}"/>
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimaryColor}"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}"/>
<LinearGradientBrush x:Key="SidebarGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="{StaticResource SidebarTopColor}" Offset="0"/>
<GradientStop Color="{StaticResource SidebarBottomColor}" Offset="1"/>
</LinearGradientBrush>
<!-- ═══════════════════════════════════════════
TYPOGRAPHY
═══════════════════════════════════════════ -->
<FontFamily x:Key="UiFont">Segoe UI</FontFamily>
<!-- ═══════════════════════════════════════════
BUTTON STYLES
═══════════════════════════════════════════ -->
<!-- Primärer Akzent-Button -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="24,10"/>
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background"
Value="{StaticResource AccentHoverBrush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background"
Value="{StaticResource AccentPressBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Background" Value="#BDBDBD"/>
<Setter Property="Foreground" Value="#888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Sekundärer Button (Ghost) -->
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="20,10"/>
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#F5F5F5"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#EBEBEB"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#BDBDBD"/>
<Setter TargetName="border" Property="BorderBrush" Value="#E0E0E0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Icon-Button für Folder-Browse -->
<Style x:Key="IconButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="Width" Value="36"/>
<Setter Property="Height" Value="36"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="#F0F7FF"/>
<Setter TargetName="bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═══════════════════════════════════════════
TEXTBOX STYLE
═══════════════════════════════════════════ -->
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost" Margin="2,0"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush"
Value="{StaticResource AccentBrush}"/>
<Setter TargetName="border" Property="BorderThickness" Value="2"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#BDBDBD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═══════════════════════════════════════════
CHECKBOX STYLE
═══════════════════════════════════════════ -->
<Style x:Key="ModernCheckBoxStyle" TargetType="CheckBox">
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border x:Name="box"
Width="18" Height="18"
CornerRadius="4"
BorderThickness="2"
BorderBrush="{StaticResource BorderBrush}"
Background="White"
Margin="0,0,8,0">
<TextBlock x:Name="check"
Text="&#xE73E;"
FontFamily="Segoe MDL2 Assets"
FontSize="11"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed"/>
</Border>
<ContentPresenter VerticalAlignment="Center"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="box" Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
<Setter TargetName="check" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═══════════════════════════════════════════
LABELS & TYPOGRAPHY
═══════════════════════════════════════════ -->
<Style x:Key="PageTitleStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
<Style x:Key="PageSubtitleStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Margin" Value="0,0,0,24"/>
<Setter Property="LineHeight" Value="20"/>
</Style>
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<!-- ═══════════════════════════════════════════
SIDEBAR STEP ITEM
═══════════════════════════════════════════ -->
<Style x:Key="StepItemStyle" TargetType="ContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="36"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Kreis-Indikator -->
<Border x:Name="circle"
Width="28" Height="28"
CornerRadius="14"
Background="#2A3050"
HorizontalAlignment="Center">
<TextBlock x:Name="iconText"
Text="{Binding Icon}"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
Foreground="#8899BB"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- Titel -->
<TextBlock x:Name="titleText"
Grid.Column="1"
Text="{Binding Title}"
FontFamily="{StaticResource UiFont}"
FontSize="12"
Foreground="#8899BB"
VerticalAlignment="Center"
Margin="8,0,0,0"/>
</Grid>
<ControlTemplate.Triggers>
<!-- Aktiver Schritt -->
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter TargetName="circle" Property="Background" Value="{StaticResource AccentBrush}"/>
<Setter TargetName="iconText" Property="Foreground" Value="White"/>
<Setter TargetName="titleText" Property="Foreground" Value="White"/>
<Setter TargetName="titleText" Property="FontWeight" Value="SemiBold"/>
</DataTrigger>
<!-- Abgeschlossener Schritt -->
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter TargetName="circle" Property="Background" Value="#107C10"/>
<Setter TargetName="iconText" Property="Text" Value="&#xE73E;"/>
<Setter TargetName="iconText" Property="Foreground" Value="White"/>
<Setter TargetName="titleText" Property="Foreground" Value="#88CCAA"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ═══════════════════════════════════════════
INFO CARD (für Zusammenfassung etc.)
═══════════════════════════════════════════ -->
<Style x:Key="InfoCardStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,12"/>
</Style>
<!-- ═══════════════════════════════════════════
PROGRESS BAR
═══════════════════════════════════════════ -->
<Style x:Key="ModernProgressBarStyle" TargetType="ProgressBar">
<Setter Property="Height" Value="6"/>
<Setter Property="Background" Value="#E0E0E0"/>
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Grid>
<Border Background="{TemplateBinding Background}"
CornerRadius="3"/>
<Border x:Name="PART_Track" CornerRadius="3" ClipToBounds="True">
<Border x:Name="PART_Indicator"
Background="{TemplateBinding Foreground}"
CornerRadius="3"
HorizontalAlignment="Left"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,36 @@
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
namespace EngineeringSync.Setup.ViewModels;
/// <summary>
/// Zentrales Datenmodell das durch alle Wizard-Schritte weitergereicht wird.
/// </summary>
public partial class WizardState : ObservableObject
{
// --- Installationspfad ---
[ObservableProperty]
private string _installPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"EngineeringSync");
// --- Erstes Projekt ---
[ObservableProperty] private string _projectName = "Mein Projekt";
[ObservableProperty] private string _engineeringPath = string.Empty;
[ObservableProperty] private string _simulationPath = string.Empty;
[ObservableProperty] private bool _watchAllFiles = false;
[ObservableProperty] private string _fileExtensions = ".jt,.cojt,.xml";
// --- Service-Optionen ---
[ObservableProperty] private bool _autoStartService = true;
[ObservableProperty] private bool _autoStartTrayApp = true;
[ObservableProperty] private bool _startAfterInstall = true;
[ObservableProperty] private bool _createDesktopShortcut = true;
[ObservableProperty] private bool _createStartMenuEntry = true;
// --- Backup-Einstellungen ---
[ObservableProperty] private bool _backupEnabled = true;
[ObservableProperty] private bool _backupUseCustomPath = false;
[ObservableProperty] private string _backupCustomPath = string.Empty;
[ObservableProperty] private int _maxBackupsPerFile = 0;
}

View File

@@ -0,0 +1,135 @@
using System.Collections.ObjectModel;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using EngineeringSync.Setup.Services;
using EngineeringSync.Setup.Views.Pages;
namespace EngineeringSync.Setup.ViewModels;
public partial class WizardViewModel : ObservableObject
{
public WizardState State { get; } = new();
[ObservableProperty] private WizardPageBase _currentPage = null!;
[ObservableProperty] private int _currentStepIndex;
[ObservableProperty] private bool _canGoBack;
[ObservableProperty] private bool _canGoNext = true;
[ObservableProperty] private bool _isLastStep;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowNextButton))]
private bool _isInstalling;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowNextButton))]
private bool _isCompleted;
public bool ShowNextButton => !IsCompleted && !IsInstalling;
public ObservableCollection<WizardStep> Steps { get; } =
[
new("Willkommen", "\uE80F"), // Home icon
new("Installation", "\uE7B7"), // Folder icon
new("Erstes Projekt","\uE8B7"), // Link icon
new("Backup", "\uE72E"), // Save icon (NEU)
new("Optionen", "\uE713"), // Settings icon
new("Zusammenfassung","\uE8A9"), // List icon
new("Installation", "\uE896"), // Download icon
new("Fertig", "\uE930"), // Completed icon
];
private readonly List<Func<WizardPageBase>> _pageFactories;
private readonly InstallerService _installer;
public WizardViewModel()
{
_installer = new InstallerService(State);
_pageFactories =
[
() => new WelcomePage(this),
() => new InstallPathPage(this),
() => new FirstProjectPage(this),
() => new BackupOptionsPage(this), // NEU Index 3
() => new ServiceOptionsPage(this),
() => new SummaryPage(this),
() => new InstallingPage(this, _installer),
() => new CompletionPage(this),
];
NavigateTo(0);
}
public void NavigateTo(int index)
{
CurrentStepIndex = index;
CurrentPage = _pageFactories[index]();
Steps[index].IsActive = true;
for (int i = 0; i < Steps.Count; i++)
{
Steps[i].IsCompleted = i < index;
Steps[i].IsActive = i == index;
}
CanGoBack = index > 0 && index < Steps.Count - 1 && !IsInstalling;
IsLastStep = index == Steps.Count - 2; // "Zusammenfassung" ist letzter normaler Schritt
// IsInstalling wird NICHT hier gesetzt wird von InstallingPage kontrolliert
IsCompleted = index == Steps.Count - 1;
CanGoNext = !IsInstalling && !IsCompleted;
}
public void SetInstallingState(bool installing)
{
IsInstalling = installing;
// CanGoNext wird durch die Bedingung !IsInstalling && !IsCompleted neu evaluiert
CanGoNext = !IsInstalling && !IsCompleted;
// GoBackCommand und CancelCommand CanExecute Bedingungen neu evaluieren
GoBackCommand.NotifyCanExecuteChanged();
CancelCommand.NotifyCanExecuteChanged();
}
[RelayCommand(CanExecute = nameof(CanExecuteGoBack))]
private void GoBack() => NavigateTo(CurrentStepIndex - 1);
private bool CanExecuteGoBack => CanGoBack && !IsInstalling;
[RelayCommand]
private void GoNext()
{
if (!CurrentPage.Validate()) return;
if (CurrentStepIndex < _pageFactories.Count - 1)
NavigateTo(CurrentStepIndex + 1);
}
[RelayCommand(CanExecute = nameof(CanExecuteCancel))]
private void Cancel()
{
if (IsCompleted) return;
var result = MessageBox.Show(
"Setup wirklich abbrechen?",
"EngineeringSync Setup",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
Application.Current.Shutdown();
}
private bool CanExecuteCancel => !IsInstalling && !IsCompleted;
[RelayCommand]
private void Finish()
{
if (State.StartAfterInstall)
_installer.LaunchTrayApp();
Application.Current.Shutdown();
}
}
public partial class WizardStep(string title, string icon) : ObservableObject
{
public string Title { get; } = title;
public string Icon { get; } = icon;
[ObservableProperty] private bool _isActive;
[ObservableProperty] private bool _isCompleted;
}

View File

@@ -0,0 +1,86 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.BackupOptionsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Backup-Einstellungen"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Legen Sie fest, ob und wie Sicherungskopien vor dem Überschreiben von Simulationsdateien erstellt werden."/>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Master-Toggle -->
<local:OptionCard Icon="&#xE72E;" Title="Backups aktivieren"
Description="Vor jeder Dateiüberschreibung wird automatisch eine .bak-Sicherungskopie erstellt"
IsChecked="{Binding BackupEnabled, Mode=TwoWay}"/>
<!-- Optionen (nur wenn Backup aktiviert) -->
<StackPanel Visibility="{Binding BackupEnabled,
Converter={StaticResource BoolToVisConverter}}">
<!-- Speicherort -->
<TextBlock Text="SPEICHERORT" Style="{StaticResource FieldLabelStyle}"
Margin="0,16,0,8"/>
<!-- Mode=OneWay: gegenseitiger Ausschluss läuft über GroupName + TwoWay am zweiten Radio -->
<RadioButton GroupName="BackupLocation"
Content="Gleicher Ordner wie die Simulationsdatei"
IsChecked="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToInvVisConverter},
Mode=OneWay}"
FontFamily="Segoe UI" FontSize="13" Margin="0,0,0,8"/>
<RadioButton GroupName="BackupLocation"
Content="Eigener Backup-Ordner"
IsChecked="{Binding BackupUseCustomPath, Mode=TwoWay}"
FontFamily="Segoe UI" FontSize="13" Margin="0,0,0,8"/>
<!-- Pfad-Eingabe (nur bei eigenem Ordner) -->
<DockPanel Margin="0,0,0,16"
Visibility="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToVisConverter}}">
<Button DockPanel.Dock="Right" Style="{StaticResource IconButtonStyle}"
Margin="6,0,0,0" Click="BrowseBackupPath_Click"
ToolTip="Backup-Verzeichnis wählen">
<TextBlock Text="&#xED25;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#0078D4"/>
</Button>
<TextBox Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding BackupCustomPath, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</DockPanel>
<!-- Aufbewahrung -->
<TextBlock Text="AUFBEWAHRUNG" Style="{StaticResource FieldLabelStyle}"
Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="Maximal" FontFamily="Segoe UI" FontSize="13"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox x:Name="MaxBackupsBox"
Width="60" Height="32" Padding="6,0"
FontFamily="Segoe UI" FontSize="13"
Text="{Binding MaxBackupsPerFile, UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center"/>
<TextBlock Text="Backups pro Datei" FontFamily="Segoe UI" FontSize="13"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<TextBlock Text="(0 = unbegrenzt, alle Backups behalten)"
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F"
Margin="0,2,0,0"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,39 @@
using System.Windows;
using EngineeringSync.Setup.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.Setup.Views.Pages;
public partial class BackupOptionsPage : WizardPageBase
{
public BackupOptionsPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
private void BrowseBackupPath_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "Backup-Verzeichnis wählen",
InitialDirectory = string.IsNullOrEmpty(Wizard.State.BackupCustomPath)
? null
: Wizard.State.BackupCustomPath
};
if (dlg.ShowDialog() == true)
Wizard.State.BackupCustomPath = dlg.FolderName;
}
public override bool Validate()
{
if (Wizard.State.BackupEnabled &&
Wizard.State.BackupUseCustomPath &&
string.IsNullOrWhiteSpace(Wizard.State.BackupCustomPath))
{
MessageBox.Show("Bitte wählen Sie einen Backup-Ordner oder deaktivieren Sie die Option 'Eigener Backup-Ordner'.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,62 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.CompletionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"
MaxWidth="400" Margin="40">
<!-- Großes Erfolgs-Icon -->
<Border Width="80" Height="80" CornerRadius="40"
HorizontalAlignment="Center" Margin="0,0,0,24">
<Border.Background>
<RadialGradientBrush>
<GradientStop Color="#E8F5E9" Offset="0"/>
<GradientStop Color="#C8E6C9" Offset="1"/>
</RadialGradientBrush>
</Border.Background>
<TextBlock Text="&#xE930;" FontFamily="Segoe MDL2 Assets" FontSize="40"
Foreground="#107C10" HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="Installation erfolgreich!"
FontFamily="Segoe UI" FontSize="22" FontWeight="Light"
Foreground="#1A1A1A" HorizontalAlignment="Center"
Margin="0,0,0,12"/>
<TextBlock FontFamily="Segoe UI" FontSize="13" Foreground="#5F5F5F"
TextAlignment="Center" TextWrapping="Wrap" LineHeight="22"
Margin="0,0,0,32">
<Run Text="EngineeringSync wurde erfolgreich installiert und konfiguriert."/>
<LineBreak/>
<Run Text="Der Windows-Dienst läuft im Hintergrund und überwacht Ihren Engineering-Ordner."/>
</TextBlock>
<!-- Status-Chips -->
<WrapPanel HorizontalAlignment="Center" Margin="0,0,0,32">
<Border Background="#E8F5E9" CornerRadius="12" Padding="12,6" Margin="4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE8FE;" FontFamily="Segoe MDL2 Assets"
FontSize="11" Foreground="#107C10" Margin="0,0,6,0"/>
<TextBlock Text="Dienst aktiv" FontFamily="Segoe UI"
FontSize="12" Foreground="#107C10"/>
</StackPanel>
</Border>
<Border Background="#E3F2FD" CornerRadius="12" Padding="12,6" Margin="4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE756;" FontFamily="Segoe MDL2 Assets"
FontSize="11" Foreground="#0078D4" Margin="0,0,6,0"/>
<TextBlock Text="Tray-App startet" FontFamily="Segoe UI"
FontSize="12" Foreground="#0078D4"/>
</StackPanel>
</Border>
</WrapPanel>
<TextBlock FontFamily="Segoe UI" FontSize="12" Foreground="#888888"
TextAlignment="Center" TextWrapping="Wrap"
Text="Klicken Sie auf &quot;Schließen&quot; um den Assistenten zu beenden."/>
</StackPanel>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,11 @@
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
public partial class CompletionPage : WizardPageBase
{
public CompletionPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,17 @@
<UserControl x:Class="EngineeringSync.Setup.Views.Pages.FeatureRow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="0,0,0,12">
<StackPanel Orientation="Horizontal">
<Border Width="36" Height="36" CornerRadius="8" Background="#F0F7FF" Margin="0,0,12,0">
<TextBlock x:Name="IconText" FontFamily="Segoe MDL2 Assets" FontSize="16"
Foreground="#0078D4" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TitleText" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A"/>
<TextBlock x:Name="DescText" FontFamily="Segoe UI" FontSize="11"
Foreground="#5F5F5F" TextWrapping="Wrap" MaxWidth="420"/>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,25 @@
using System.Windows;
using System.Windows.Controls;
namespace EngineeringSync.Setup.Views.Pages;
public partial class FeatureRow : UserControl
{
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(FeatureRow),
new PropertyMetadata(string.Empty, (d, e) => ((FeatureRow)d).IconText.Text = (string)e.NewValue));
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(FeatureRow),
new PropertyMetadata(string.Empty, (d, e) => ((FeatureRow)d).TitleText.Text = (string)e.NewValue));
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.Register(nameof(Description), typeof(string), typeof(FeatureRow),
new PropertyMetadata(string.Empty, (d, e) => ((FeatureRow)d).DescText.Text = (string)e.NewValue));
public string Icon { get => (string)GetValue(IconProperty); set => SetValue(IconProperty, value); }
public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); }
public FeatureRow() => InitializeComponent();
}

View File

@@ -0,0 +1,138 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.FirstProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Erstes Projekt konfigurieren"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Definieren Sie Ihr erstes überwachtes Projekt. Sie können später über die Tray-App beliebig viele weitere Projekte hinzufügen."/>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Projektname -->
<StackPanel Margin="0,0,0,16">
<TextBlock Text="PROJEKTNAME" Style="{StaticResource FieldLabelStyle}"/>
<TextBox Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding ProjectName, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</StackPanel>
<!-- Engineering-Pfad -->
<StackPanel Margin="0,0,0,16">
<TextBlock Style="{StaticResource FieldLabelStyle}">
<Run Text="ENGINEERING-QUELLPFAD "/>
<Run Text="(wird überwacht)" Foreground="#0078D4" FontWeight="Normal"/>
</TextBlock>
<DockPanel>
<Button DockPanel.Dock="Right" Style="{StaticResource IconButtonStyle}"
Margin="6,0,0,0" Click="BrowseEngineering_Click"
ToolTip="Engineering-Verzeichnis wählen">
<TextBlock Text="&#xED25;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#0078D4"/>
</Button>
<TextBox Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding EngineeringPath, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</DockPanel>
<TextBlock Text="Änderungen in diesem Verzeichnis werden protokolliert und dem Simulations-Ingenieur gemeldet."
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F" Margin="0,4,0,0"/>
</StackPanel>
<!-- Simulations-Pfad -->
<StackPanel Margin="0,0,0,16">
<TextBlock Style="{StaticResource FieldLabelStyle}">
<Run Text="SIMULATIONS-ZIELPFAD "/>
<Run Text="(Sync-Ziel)" Foreground="#107C10" FontWeight="Normal"/>
</TextBlock>
<DockPanel>
<Button DockPanel.Dock="Right" Style="{StaticResource IconButtonStyle}"
Margin="6,0,0,0" Click="BrowseSimulation_Click"
ToolTip="Simulations-Verzeichnis wählen">
<TextBlock Text="&#xED25;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#107C10"/>
</Button>
<TextBox Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding SimulationPath, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</DockPanel>
<TextBlock Text="Freigegebene Änderungen werden kontrolliert in dieses Verzeichnis kopiert (mit automatischem Backup)."
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F" Margin="0,4,0,0"/>
</StackPanel>
<!-- Dateiüberwachung -->
<StackPanel Margin="0,0,0,16">
<TextBlock Text="ÜBERWACHTE DATEIEN" Style="{StaticResource FieldLabelStyle}"/>
<!-- Option: Alle Dateien -->
<Border Background="#F0F8FF" CornerRadius="6" Padding="12,10" Margin="0,4,0,8">
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="WatchAllCheck"
IsChecked="{Binding WatchAllFiles}"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<StackPanel>
<TextBlock Text="Alle Dateitypen überwachen"
FontFamily="Segoe UI" FontSize="13" FontWeight="SemiBold"
Foreground="#1B1B2F"/>
<TextBlock Text="Jede Dateiänderung im Ordner wird protokolliert (unabhängig von der Endung)."
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Spezifische Endungen (nur wenn nicht "Alle Dateien") -->
<StackPanel Visibility="{Binding WatchAllFiles,
Converter={StaticResource BoolToInvVisConverter}}">
<TextBlock Text="DATEIENDUNGEN (komma-getrennt)"
Style="{StaticResource FieldLabelStyle}" Margin="0,0,0,6"/>
<!-- Quick-Select Buttons -->
<WrapPanel Margin="0,0,0,8">
<Button Content=".jt" Tag=".jt" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".cojt" Tag=".cojt" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".xml" Tag=".xml" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".stp" Tag=".stp" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".step" Tag=".step" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".igs" Tag=".igs" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".iges" Tag=".iges" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
<Button Content=".prt" Tag=".prt" Click="AddExt_Click"
Style="{StaticResource SecondaryButtonStyle}"
Padding="8,4" Margin="0,0,6,6" Height="26" FontSize="11"/>
</WrapPanel>
<TextBox x:Name="ExtensionsBox"
Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding FileExtensions, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
<TextBlock Text="Klicke auf einen Typ um ihn hinzuzufügen, oder tippe direkt."
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F" Margin="0,4,0,0"/>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,79 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using EngineeringSync.Setup.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.Setup.Views.Pages;
public partial class FirstProjectPage : WizardPageBase
{
public FirstProjectPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
private void BrowseEngineering_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "Engineering-Quellpfad wählen",
InitialDirectory = Wizard.State.EngineeringPath
};
if (dlg.ShowDialog() == true)
Wizard.State.EngineeringPath = dlg.FolderName;
}
private void BrowseSimulation_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "Simulations-Zielpfad wählen",
InitialDirectory = Wizard.State.SimulationPath
};
if (dlg.ShowDialog() == true)
Wizard.State.SimulationPath = dlg.FolderName;
}
private void AddExt_Click(object sender, RoutedEventArgs e)
{
var ext = (sender as Button)?.Tag?.ToString();
if (string.IsNullOrEmpty(ext)) return;
var current = Wizard.State.FileExtensions?.Trim() ?? string.Empty;
var parts = current.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (!parts.Contains(ext, StringComparer.OrdinalIgnoreCase))
Wizard.State.FileExtensions = current.Length > 0 ? current + "," + ext : ext;
}
public override bool Validate()
{
if (string.IsNullOrWhiteSpace(Wizard.State.ProjectName))
{
MessageBox.Show("Bitte geben Sie einen Projektnamen an.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
if (string.IsNullOrWhiteSpace(Wizard.State.EngineeringPath) ||
!Directory.Exists(Wizard.State.EngineeringPath))
{
MessageBox.Show("Der Engineering-Pfad existiert nicht oder ist nicht angegeben.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
if (string.IsNullOrWhiteSpace(Wizard.State.SimulationPath) ||
!Directory.Exists(Wizard.State.SimulationPath))
{
MessageBox.Show("Der Simulations-Pfad existiert nicht oder ist nicht angegeben.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
if (!Wizard.State.WatchAllFiles && string.IsNullOrWhiteSpace(Wizard.State.FileExtensions))
{
MessageBox.Show("Bitte geben Sie mindestens eine Dateiendung an oder wählen Sie 'Alle Dateitypen überwachen'.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,10 @@
<UserControl x:Class="EngineeringSync.Setup.Views.Pages.InstallItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="0,2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE73E;" FontFamily="Segoe MDL2 Assets" FontSize="10"
Foreground="#107C10" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock x:Name="ItemText" FontFamily="Consolas" FontSize="12" Foreground="#1A1A1A"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,14 @@
using System.Windows;
using System.Windows.Controls;
namespace EngineeringSync.Setup.Views.Pages;
public partial class InstallItem : UserControl
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(InstallItem),
new PropertyMetadata(string.Empty, (d, e) => ((InstallItem)d).ItemText.Text = (string)e.NewValue));
public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public InstallItem() => InitializeComponent();
}

View File

@@ -0,0 +1,61 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.InstallPathPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Installationsverzeichnis"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Wählen Sie das Verzeichnis, in dem EngineeringSync installiert werden soll. Der Windows-Dienst und die Tray-App werden dort abgelegt."/>
<!-- Pfad-Auswahl -->
<StackPanel Grid.Row="2" Margin="0,0,0,20">
<TextBlock Text="INSTALLATIONSPFAD" Style="{StaticResource FieldLabelStyle}"/>
<DockPanel>
<Button DockPanel.Dock="Right" Style="{StaticResource IconButtonStyle}"
Margin="6,0,0,0" ToolTip="Verzeichnis auswählen"
Click="Browse_Click">
<TextBlock Text="&#xED25;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#0078D4"/>
</Button>
<TextBox x:Name="PathBox"
Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding InstallPath, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</DockPanel>
<!-- Speicherplatz-Hinweis -->
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock Text="&#xE8C8;" FontFamily="Segoe MDL2 Assets"
FontSize="11" Foreground="#5F5F5F" Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="Benötigter Speicherplatz: ca. 45 MB"
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F"/>
</StackPanel>
</StackPanel>
<!-- Info-Karte -->
<Border Grid.Row="3" Style="{StaticResource InfoCardStyle}" VerticalAlignment="Top">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Text="&#xE946;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#0078D4" Margin="0,0,8,0"/>
<TextBlock Text="Folgende Komponenten werden installiert:"
FontFamily="Segoe UI" FontSize="12" FontWeight="SemiBold"
Foreground="#1A1A1A"/>
</StackPanel>
<local:InstallItem Text="EngineeringSync.Service.exe (Windows-Dienst)"/>
<local:InstallItem Text="EngineeringSync.TrayApp.exe (Tray-Anwendung)"/>
<local:InstallItem Text="engineeringsync.db (Datenbank)"/>
<local:InstallItem Text="appsettings.json (Konfiguration)"/>
</StackPanel>
</Border>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,35 @@
using System.Windows;
using EngineeringSync.Setup.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.Setup.Views.Pages;
public partial class InstallPathPage : WizardPageBase
{
public InstallPathPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
private void Browse_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "Installationsverzeichnis auswählen",
InitialDirectory = Wizard.State.InstallPath
};
if (dlg.ShowDialog() == true)
Wizard.State.InstallPath = dlg.FolderName;
}
public override bool Validate()
{
if (string.IsNullOrWhiteSpace(Wizard.State.InstallPath))
{
MessageBox.Show("Bitte wählen Sie ein Installationsverzeichnis.",
"Validierung", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,65 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.InstallingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White"
Loaded="Page_Loaded">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Installation läuft..."
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" x:Name="SubtitleText"
Style="{StaticResource PageSubtitleStyle}"
Text="Bitte warten Sie, während EngineeringSync installiert wird."/>
<!-- Fortschrittsbereich -->
<StackPanel Grid.Row="2" VerticalAlignment="Center">
<!-- Aktueller Schritt -->
<TextBlock x:Name="StepText"
FontFamily="Segoe UI" FontSize="13" FontWeight="SemiBold"
Foreground="#1A1A1A" Margin="0,0,0,8" Text="Starte Installation..."/>
<!-- Fortschrittsbalken -->
<ProgressBar x:Name="ProgressBar"
Style="{StaticResource ModernProgressBarStyle}"
Minimum="0" Maximum="100" Value="0"
Margin="0,0,0,4"/>
<Grid Margin="0,0,0,24">
<TextBlock x:Name="ProgressText" FontFamily="Segoe UI" FontSize="11"
Foreground="#5F5F5F" HorizontalAlignment="Left" Text="0%"/>
<TextBlock x:Name="TimeText" FontFamily="Segoe UI" FontSize="11"
Foreground="#5F5F5F" HorizontalAlignment="Right" Text=""/>
</Grid>
<!-- Live-Log -->
<Border Background="#1E1E1E" CornerRadius="8" Padding="16" Height="180">
<ScrollViewer x:Name="LogScrollViewer" VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="LogText"
FontFamily="Cascadia Code, Consolas, Courier New"
FontSize="11" Foreground="#CCCCCC"
TextWrapping="Wrap" LineHeight="18"/>
</ScrollViewer>
</Border>
</StackPanel>
<!-- Erfolgs/Fehler-Banner -->
<Border Grid.Row="3" x:Name="ResultBanner"
CornerRadius="8" Padding="14,10" Margin="0,16,0,0"
Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="ResultIcon" FontFamily="Segoe MDL2 Assets"
FontSize="16" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock x:Name="ResultText" FontFamily="Segoe UI" FontSize="12"
VerticalAlignment="Center" TextWrapping="Wrap"/>
</StackPanel>
</Border>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,102 @@
using System.Windows;
using System.Windows.Media;
using EngineeringSync.Setup.Services;
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
public partial class InstallingPage : WizardPageBase
{
private readonly InstallerService _installer;
private bool _started;
public InstallingPage(WizardViewModel wizard, InstallerService installer) : base(wizard)
{
_installer = installer;
InitializeComponent();
}
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
// Nur einmal starten (Page kann bei Navigation neu erzeugt werden)
if (_started) return;
_started = true;
_installer.Progress += OnProgress;
_installer.LogMessage += OnLogMessage;
try
{
// Button während Installation deaktivieren
Wizard.SetInstallingState(true);
await _installer.InstallAsync();
ShowSuccess();
// Buttons wieder aktivieren vor Navigation
Wizard.SetInstallingState(false);
Wizard.NavigateTo(7); // → CompletionPage
}
catch (Exception ex)
{
ShowError(ex.Message);
// Buttons wieder aktivieren im Fehlerfall
Wizard.SetInstallingState(false);
}
finally
{
_installer.Progress -= OnProgress;
_installer.LogMessage -= OnLogMessage;
}
}
private void OnProgress(int percent, string step)
{
Dispatcher.Invoke(() =>
{
ProgressBar.Value = percent;
ProgressText.Text = $"{percent}%";
StepText.Text = step;
});
}
private void OnLogMessage(string message)
{
Dispatcher.Invoke(() =>
{
var ts = DateTime.Now.ToString("HH:mm:ss");
LogText.Text += $"[{ts}] {message}\n";
LogScrollViewer.ScrollToEnd();
});
}
private void ShowSuccess()
{
Dispatcher.Invoke(() =>
{
ResultBanner.Background = new SolidColorBrush(Color.FromRgb(232, 245, 233));
ResultIcon.Text = "\uE73E";
ResultIcon.Foreground = new SolidColorBrush(Color.FromRgb(16, 124, 16));
ResultText.Text = "Installation erfolgreich abgeschlossen.";
ResultText.Foreground = new SolidColorBrush(Color.FromRgb(16, 124, 16));
ResultBanner.Visibility = Visibility.Visible;
SubtitleText.Text = "Alle Komponenten wurden erfolgreich installiert.";
ProgressBar.Value = 100;
ProgressText.Text = "100%";
});
}
private void ShowError(string message)
{
Dispatcher.Invoke(() =>
{
ResultBanner.Background = new SolidColorBrush(Color.FromRgb(255, 235, 238));
ResultIcon.Text = "\uE783";
ResultIcon.Foreground = new SolidColorBrush(Color.FromRgb(196, 43, 28));
ResultText.Text = $"Fehler: {message}";
ResultText.Foreground = new SolidColorBrush(Color.FromRgb(196, 43, 28));
ResultBanner.Visibility = Visibility.Visible;
SubtitleText.Text = "Die Installation konnte nicht abgeschlossen werden.";
});
}
}

View File

@@ -0,0 +1,32 @@
<UserControl x:Class="EngineeringSync.Setup.Views.Pages.OptionCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="0,0,0,8">
<Border x:Name="CardBorder" Background="White" BorderThickness="1"
BorderBrush="#E0E0E0" CornerRadius="8" Padding="16,12"
Cursor="Hand" MouseLeftButtonDown="Card_Click">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="32" Height="32" CornerRadius="8" Background="#F0F7FF">
<TextBlock x:Name="IconText" FontFamily="Segoe MDL2 Assets" FontSize="15"
Foreground="#0078D4" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="12,0">
<TextBlock x:Name="TitleText" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A"/>
<TextBlock x:Name="DescText" FontFamily="Segoe UI" FontSize="11"
Foreground="#5F5F5F" TextWrapping="Wrap"/>
</StackPanel>
<CheckBox x:Name="ToggleBox" Grid.Column="2"
Style="{StaticResource ModernCheckBoxStyle}"
VerticalAlignment="Center" IsHitTestVisible="False"/>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,47 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace EngineeringSync.Setup.Views.Pages;
public partial class OptionCard : UserControl
{
public static readonly DependencyProperty IconProperty =
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(OptionCard),
new PropertyMetadata(string.Empty, (d, e) => ((OptionCard)d).IconText.Text = (string)e.NewValue));
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(OptionCard),
new PropertyMetadata(string.Empty, (d, e) => ((OptionCard)d).TitleText.Text = (string)e.NewValue));
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.Register(nameof(Description), typeof(string), typeof(OptionCard),
new PropertyMetadata(string.Empty, (d, e) => ((OptionCard)d).DescText.Text = (string)e.NewValue));
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(OptionCard),
new FrameworkPropertyMetadata(true,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
(d, e) => ((OptionCard)d).UpdateVisual((bool)e.NewValue)));
public string Icon { get => (string)GetValue(IconProperty); set => SetValue(IconProperty, value); }
public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); }
public bool IsChecked { get => (bool)GetValue(IsCheckedProperty); set => SetValue(IsCheckedProperty, value); }
public OptionCard() => InitializeComponent();
private void Card_Click(object sender, RoutedEventArgs e) => IsChecked = !IsChecked;
private void UpdateVisual(bool isChecked)
{
if (ToggleBox is null) return;
ToggleBox.IsChecked = isChecked;
CardBorder.BorderBrush = isChecked
? new SolidColorBrush(Color.FromRgb(0, 120, 212))
: new SolidColorBrush(Color.FromRgb(224, 224, 224));
CardBorder.Background = isChecked
? new SolidColorBrush(Color.FromArgb(15, 0, 120, 212))
: new SolidColorBrush(Colors.White);
}
}

View File

@@ -0,0 +1,51 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.ServiceOptionsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Start- und Verknüpfungs-Optionen"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Legen Sie fest, wie EngineeringSync beim Start von Windows und nach der Installation verhalten soll."/>
<StackPanel Grid.Row="2">
<!-- Autostart-Gruppe -->
<TextBlock Text="AUTOSTART" Style="{StaticResource FieldLabelStyle}" Margin="0,0,0,10"/>
<local:OptionCard Icon="&#xE8FE;" Title="Windows-Dienst automatisch starten"
Description="Der EngineeringSync-Dienst startet automatisch mit Windows (empfohlen)"
IsChecked="{Binding AutoStartService, Mode=TwoWay}"/>
<local:OptionCard Icon="&#xE756;" Title="Tray-App bei Anmeldung starten"
Description="Die Benachrichtigungs-App erscheint automatisch im Systemtray nach dem Login"
IsChecked="{Binding AutoStartTrayApp, Mode=TwoWay}"/>
<!-- Verknüpfungen -->
<TextBlock Text="VERKNÜPFUNGEN" Style="{StaticResource FieldLabelStyle}" Margin="0,20,0,10"/>
<local:OptionCard Icon="&#xE7C5;" Title="Desktop-Verknüpfung erstellen"
Description="Erstellt eine Verknüpfung zur Tray-App auf dem Desktop"
IsChecked="{Binding CreateDesktopShortcut, Mode=TwoWay}"/>
<local:OptionCard Icon="&#xE8A9;" Title="Startmenü-Eintrag erstellen"
Description="Fügt EngineeringSync dem Windows-Startmenü hinzu"
IsChecked="{Binding CreateStartMenuEntry, Mode=TwoWay}"/>
<!-- Nach Installation -->
<TextBlock Text="NACH INSTALLATION" Style="{StaticResource FieldLabelStyle}" Margin="0,20,0,10"/>
<local:OptionCard Icon="&#xE768;" Title="Tray-App nach Installation starten"
Description="Startet die Tray-App direkt nach Abschluss der Installation"
IsChecked="{Binding StartAfterInstall, Mode=TwoWay}"/>
</StackPanel>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,11 @@
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
public partial class ServiceOptionsPage : WizardPageBase
{
public ServiceOptionsPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,18 @@
<UserControl x:Class="EngineeringSync.Setup.Views.Pages.SummaryBoolRow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="LabelText" Grid.Column="0"
FontFamily="Segoe UI" FontSize="12" Foreground="#5F5F5F"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock x:Name="CheckIcon" FontFamily="Segoe MDL2 Assets" FontSize="11"
Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock x:Name="ValueText" FontFamily="Segoe UI" FontSize="12"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,40 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace EngineeringSync.Setup.Views.Pages;
public partial class SummaryBoolRow : UserControl
{
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register(nameof(Label), typeof(string), typeof(SummaryBoolRow),
new PropertyMetadata(string.Empty, (d, e) => ((SummaryBoolRow)d).LabelText.Text = (string)e.NewValue));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(bool), typeof(SummaryBoolRow),
new PropertyMetadata(false, (d, e) => ((SummaryBoolRow)d).UpdateVisual((bool)e.NewValue)));
public string Label { get => (string)GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
public bool Value { get => (bool)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
public SummaryBoolRow() => InitializeComponent();
private void UpdateVisual(bool isOn)
{
if (CheckIcon is null) return;
if (isOn)
{
CheckIcon.Text = "\uE73E";
CheckIcon.Foreground = new SolidColorBrush(Color.FromRgb(16, 124, 16));
ValueText.Text = "Ja";
ValueText.Foreground = new SolidColorBrush(Color.FromRgb(16, 124, 16));
}
else
{
CheckIcon.Text = "\uE711";
CheckIcon.Foreground = new SolidColorBrush(Color.FromRgb(160, 160, 160));
ValueText.Text = "Nein";
ValueText.Foreground = new SolidColorBrush(Color.FromRgb(160, 160, 160));
}
}
}

View File

@@ -0,0 +1,92 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.SummaryPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Zusammenfassung" Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Überprüfen Sie Ihre Einstellungen. Klicken Sie auf &quot;Jetzt installieren&quot; um die Installation zu starten."/>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Installation -->
<Border Style="{StaticResource InfoCardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="&#xE7B7;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Installation" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A" VerticalAlignment="Center"/>
</StackPanel>
<local:SummaryRow Label="Installationspfad" Value="{Binding InstallPath}"/>
</StackPanel>
</Border>
<!-- Erstes Projekt -->
<Border Style="{StaticResource InfoCardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="&#xE8B7;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Erstes Projekt" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A" VerticalAlignment="Center"/>
</StackPanel>
<local:SummaryRow Label="Projektname" Value="{Binding ProjectName}"/>
<local:SummaryRow Label="Engineering-Pfad" Value="{Binding EngineeringPath}"/>
<local:SummaryRow Label="Simulations-Pfad" Value="{Binding SimulationPath}"/>
<local:SummaryRow Label="Dateiendungen" Value="{Binding FileExtensions}"/>
</StackPanel>
</Border>
<!-- Backup -->
<Border Style="{StaticResource InfoCardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="&#xE72E;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Backup" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A" VerticalAlignment="Center"/>
</StackPanel>
<local:SummaryBoolRow Label="Backups aktiviert" Value="{Binding BackupEnabled}"/>
<local:SummaryRow Label="Backup-Ordner"
Value="{Binding BackupCustomPath}"
Visibility="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToVisConverter}}"/>
<!-- Aufbewahrung immer anzeigen wenn Backup aktiv (0 = unbegrenzt ist ein valider Wert) -->
<local:SummaryRow Label="Max. Backups/Datei"
Value="{Binding MaxBackupsPerFile,
StringFormat='{}{0} (0=unbegrenzt)'}"
Visibility="{Binding BackupEnabled,
Converter={StaticResource BoolToVisConverter}}"/>
</StackPanel>
</Border>
<!-- Optionen -->
<Border Style="{StaticResource InfoCardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="&#xE713;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Optionen" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A" VerticalAlignment="Center"/>
</StackPanel>
<local:SummaryBoolRow Label="Dienst automatisch starten" Value="{Binding AutoStartService}"/>
<local:SummaryBoolRow Label="Tray-App bei Anmeldung" Value="{Binding AutoStartTrayApp}"/>
<local:SummaryBoolRow Label="Desktop-Verknüpfung" Value="{Binding CreateDesktopShortcut}"/>
<local:SummaryBoolRow Label="Startmenü-Eintrag" Value="{Binding CreateStartMenuEntry}"/>
<local:SummaryBoolRow Label="Tray-App nach Installation" Value="{Binding StartAfterInstall}"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,11 @@
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
public partial class SummaryPage : WizardPageBase
{
public SummaryPage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,16 @@
<UserControl x:Class="EngineeringSync.Setup.Views.Pages.SummaryRow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Margin="0,2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="LabelText" Grid.Column="0"
FontFamily="Segoe UI" FontSize="12" Foreground="#5F5F5F"/>
<TextBlock x:Name="ValueText" Grid.Column="1"
FontFamily="Segoe UI" FontSize="12" Foreground="#1A1A1A"
TextWrapping="Wrap"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,19 @@
using System.Windows;
using System.Windows.Controls;
namespace EngineeringSync.Setup.Views.Pages;
public partial class SummaryRow : UserControl
{
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register(nameof(Label), typeof(string), typeof(SummaryRow),
new PropertyMetadata(string.Empty, (d, e) => ((SummaryRow)d).LabelText.Text = (string)e.NewValue));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof(string), typeof(SummaryRow),
new PropertyMetadata(string.Empty, (d, e) => ((SummaryRow)d).ValueText.Text = (string)e.NewValue));
public string Label { get => (string)GetValue(LabelProperty); set => SetValue(LabelProperty, value); }
public string Value { get => (string)GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
public SummaryRow() => InitializeComponent();
}

View File

@@ -0,0 +1,63 @@
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.WelcomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Großes Begrüßungs-Icon -->
<Border Grid.Row="0" Width="64" Height="64" CornerRadius="16"
HorizontalAlignment="Left" Margin="0,0,0,24">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#0078D4" Offset="0"/>
<GradientStop Color="#00B4FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<TextBlock Text="&#xE8B7;" FontFamily="Segoe MDL2 Assets" FontSize="32"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- Überschrift -->
<StackPanel Grid.Row="1" Margin="0,0,0,20">
<TextBlock Text="Willkommen bei EngineeringSync"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Style="{StaticResource PageSubtitleStyle}"
Text="Dieser Assistent führt Sie durch die Installation und Erstkonfiguration von EngineeringSync dem intelligenten Bindeglied zwischen Ihrem Engineering- und Simulationsumfeld."/>
</StackPanel>
<!-- Feature-Liste -->
<StackPanel Grid.Row="2" VerticalAlignment="Top">
<TextBlock Text="Was wird installiert:"
FontFamily="Segoe UI" FontSize="12" FontWeight="SemiBold"
Foreground="#5F5F5F" Margin="0,0,0,12"/>
<local:FeatureRow Icon="&#xE8A0;" Title="EngineeringSync Service"
Description="Windows-Dienst der Ihr Engineering-Verzeichnis überwacht und Änderungen protokolliert"/>
<local:FeatureRow Icon="&#xE756;" Title="System Tray App"
Description="Benachrichtigt Sie sofort über neue Änderungen und ermöglicht kontrollierten Sync"/>
<local:FeatureRow Icon="&#xE713;" Title="Konfigurations-Wizard"
Description="Verwalten Sie Projekte und Sync-Pfade bequem per grafischer Oberfläche"/>
</StackPanel>
<!-- System-Check -->
<Border Grid.Row="3" Background="#F0F7FF" CornerRadius="8" Padding="14,10" Margin="0,16,0,0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE946;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBlock FontFamily="Segoe UI" FontSize="12" Foreground="#5F5F5F"
VerticalAlignment="Center" TextWrapping="Wrap">
<Run Text="Klicken Sie auf " FontWeight="Regular"/>
<Run Text="Weiter" FontWeight="SemiBold"/>
<Run Text=" um die Installation zu beginnen." FontWeight="Regular"/>
</TextBlock>
</StackPanel>
</Border>
</Grid>
</local:WizardPageBase>

View File

@@ -0,0 +1,11 @@
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
public partial class WelcomePage : WizardPageBase
{
public WelcomePage(WizardViewModel wizard) : base(wizard)
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,25 @@
using System.Windows.Controls;
using EngineeringSync.Setup.ViewModels;
namespace EngineeringSync.Setup.Views.Pages;
/// <summary>
/// Basisklasse für alle Wizard-Seiten.
/// Stellt gemeinsame WizardViewModel-Referenz und Validierungsschnittstelle bereit.
/// </summary>
public abstract class WizardPageBase : UserControl
{
protected WizardViewModel Wizard { get; }
protected WizardPageBase(WizardViewModel wizard)
{
Wizard = wizard;
DataContext = wizard.State;
}
/// <summary>
/// Wird vor dem Vorwärtsnavigieren aufgerufen.
/// Gibt false zurück um die Navigation zu blockieren (z.B. Validierungsfehler).
/// </summary>
public virtual bool Validate() => true;
}

View File

@@ -0,0 +1,251 @@
<Window x:Class="EngineeringSync.Setup.Views.WizardWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:EngineeringSync.Setup.ViewModels"
Title="EngineeringSync Setup"
Width="860" Height="580"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent">
<Window.DataContext>
<vm:WizardViewModel/>
</Window.DataContext>
<!-- Äußerer Schatten-Container -->
<Border CornerRadius="10"
Background="White">
<Border.Effect>
<DropShadowEffect Color="#40000000" BlurRadius="30" ShadowDepth="0" Opacity="0.4"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ═══════════════════════════════
LINKE SIDEBAR
═══════════════════════════════ -->
<Border Grid.Column="0"
Background="{StaticResource SidebarGradient}"
CornerRadius="10,0,0,10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Logo-Bereich -->
<StackPanel Grid.Row="0" Margin="24,32,24,32">
<!-- App-Icon (Segoe MDL2 Gear-ähnliches Symbol) -->
<Border Width="52" Height="52"
CornerRadius="12"
HorizontalAlignment="Left"
Margin="0,0,0,16">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#0078D4" Offset="0"/>
<GradientStop Color="#00B4FF" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<TextBlock Text="&#xE8B7;"
FontFamily="Segoe MDL2 Assets"
FontSize="26"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="EngineeringSync"
FontFamily="Segoe UI"
FontSize="16"
FontWeight="SemiBold"
Foreground="White"/>
<TextBlock Text="Setup-Assistent"
FontFamily="Segoe UI"
FontSize="11"
Foreground="#7788AA"/>
</StackPanel>
<!-- Schritt-Indikatoren -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding Steps}"
Margin="20,0,20,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Style="{StaticResource StepItemStyle}"
Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Version -->
<TextBlock Grid.Row="2"
Text="Version 1.0.0"
FontFamily="Segoe UI"
FontSize="10"
Foreground="#445566"
Margin="24,0,24,20"
HorizontalAlignment="Left"/>
</Grid>
</Border>
<!-- ═══════════════════════════════
RECHTE INHALTSSEITE + NAVIGATION
═══════════════════════════════ -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Custom Title Bar -->
<RowDefinition Height="*"/> <!-- Page Content -->
<RowDefinition Height="Auto"/> <!-- Navigation Bar -->
</Grid.RowDefinitions>
<!-- Custom Title Bar (Drag + Schließen) -->
<Border Grid.Row="0"
Height="40"
CornerRadius="0,10,0,0"
MouseLeftButtonDown="TitleBar_MouseDown">
<Grid>
<TextBlock Text="EngineeringSync Setup"
FontFamily="Segoe UI"
FontSize="12"
Foreground="#888888"
VerticalAlignment="Center"
Margin="16,0,0,0"/>
<Button Content="&#xE8BB;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Width="40" Height="40"
Background="Transparent"
BorderThickness="0"
Foreground="#888888"
Cursor="Hand"
Click="CloseButton_Click"
ToolTip="Schließen">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bg"
Background="Transparent"
CornerRadius="0,10,0,0">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bg" Property="Background" Value="#FFE81123"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
</Grid>
</Border>
<!-- ── Separator ── -->
<Border Grid.Row="0" Height="1" Background="{StaticResource BorderBrush}"
VerticalAlignment="Bottom"/>
<!-- Seiten-Content mit Animation -->
<ContentPresenter Grid.Row="1"
Content="{Binding CurrentPage}"
Margin="0"/>
<!-- ── Separator ── -->
<Border Grid.Row="2" Height="1" Background="{StaticResource BorderBrush}"
VerticalAlignment="Top"/>
<!-- Navigation Bar -->
<Border Grid.Row="2"
Background="#FAFAFA"
CornerRadius="0,0,10,0"
Padding="24,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Abbrechen (links) -->
<Button Grid.Column="0"
Content="Abbrechen"
HorizontalAlignment="Left"
Style="{StaticResource SecondaryButtonStyle}"
Command="{Binding CancelCommand}"
Visibility="{Binding IsCompleted,
Converter={StaticResource BoolToInvVisConverter}}"/>
<!-- Zurück -->
<Button Grid.Column="1"
Style="{StaticResource SecondaryButtonStyle}"
Margin="0,0,8,0"
Command="{Binding GoBackCommand}"
Visibility="{Binding CanGoBack,
Converter={StaticResource BoolToVisConverter}}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE72B;"
FontFamily="Segoe MDL2 Assets"
Margin="0,0,6,0"/>
<TextBlock Text="Zurück"
FontFamily="Segoe UI"/>
</StackPanel>
</Button.Content>
</Button>
<!-- Weiter / Installieren -->
<Button Grid.Column="2"
Style="{StaticResource PrimaryButtonStyle}"
Command="{Binding GoNextCommand}"
Visibility="{Binding ShowNextButton,
Converter={StaticResource BoolToVisConverter}}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="nextLabel"
Text="{Binding IsLastStep,
Converter={StaticResource LastStepLabelConverter}}"/>
<TextBlock Text=" &#xE72A;"
FontFamily="Segoe MDL2 Assets"
Visibility="{Binding IsLastStep,
Converter={StaticResource BoolToInvVisConverter}}"/>
</StackPanel>
</Button.Content>
</Button>
<!-- Fertig-Button -->
<Button Grid.Column="3"
Style="{StaticResource PrimaryButtonStyle}"
Command="{Binding FinishCommand}"
Visibility="{Binding IsCompleted,
Converter={StaticResource BoolToVisConverter}}">
<Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE73E;"
FontFamily="Segoe MDL2 Assets"
Margin="0,0,6,0"/>
<TextBlock Text="Schließen"
FontFamily="Segoe UI"/>
</StackPanel>
</Button.Content>
</Button>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,24 @@
using System.Windows;
using System.Windows.Input;
namespace EngineeringSync.Setup.Views;
public partial class WizardWindow : Window
{
public WizardWindow()
{
InitializeComponent();
}
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
DragMove();
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
var vm = (ViewModels.WizardViewModel)DataContext;
vm.CancelCommand.Execute(null);
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="EngineeringSync.Setup.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- Adminrechte für Service-Installation erforderlich -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 11 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>