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,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);
}
}