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:
11
EngineeringSync.TrayApp/App.xaml
Normal file
11
EngineeringSync.TrayApp/App.xaml
Normal file
@@ -0,0 +1,11 @@
|
||||
<Application x:Class="EngineeringSync.TrayApp.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
|
||||
ShutdownMode="OnExplicitShutdown">
|
||||
<Application.Resources>
|
||||
<tb:TaskbarIcon x:Key="TrayIcon"
|
||||
ToolTipText="EngineeringSync – Watcher aktiv"
|
||||
IconSource="/Assets/tray.ico" />
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
139
EngineeringSync.TrayApp/App.xaml.cs
Normal file
139
EngineeringSync.TrayApp/App.xaml.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using EngineeringSync.TrayApp.Services;
|
||||
using EngineeringSync.TrayApp.ViewModels;
|
||||
using EngineeringSync.TrayApp.Views;
|
||||
using H.NotifyIcon;
|
||||
|
||||
namespace EngineeringSync.TrayApp;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private TaskbarIcon? _trayIcon;
|
||||
private SignalRService? _signalR;
|
||||
private ApiClient? _apiClient;
|
||||
private PendingChangesWindow? _pendingChangesWindow;
|
||||
private PendingChangesViewModel? _pendingChangesViewModel;
|
||||
|
||||
protected override async void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
_apiClient = new ApiClient(new System.Net.Http.HttpClient());
|
||||
_signalR = new SignalRService();
|
||||
|
||||
// TaskbarIcon aus XAML-Resource laden (wird dadurch korrekt im Shell registriert)
|
||||
_trayIcon = (TaskbarIcon)FindResource("TrayIcon");
|
||||
_trayIcon.ContextMenu = BuildContextMenu();
|
||||
|
||||
// ForceCreate() aufrufen, um sicherzustellen dass das Icon im System Tray erscheint
|
||||
_trayIcon.ForceCreate(enablesEfficiencyMode: false);
|
||||
|
||||
_signalR.ChangeNotificationReceived += OnChangeNotificationReceived;
|
||||
_signalR.ChangeNotificationReceived += OnPendingChangesWindowNotification;
|
||||
|
||||
try { await _signalR.ConnectAsync(); }
|
||||
catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TrayApp] SignalR Connect Fehler: {ex}"); }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Debug-Fehlerbehandlung für Icon-Initialisierung
|
||||
MessageBox.Show(
|
||||
$"Fehler bei der Initialisierung des Tray Icons:\n\n{ex.Message}\n\n{ex.StackTrace}",
|
||||
"EngineeringSync Startup Error",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu BuildContextMenu()
|
||||
{
|
||||
var menu = new ContextMenu();
|
||||
|
||||
var showChanges = new MenuItem { Header = "Änderungen anzeigen" };
|
||||
showChanges.Click += (_, _) => OpenChangesWindow();
|
||||
menu.Items.Add(showChanges);
|
||||
|
||||
var showProjects = new MenuItem { Header = "Projekte verwalten" };
|
||||
showProjects.Click += (_, _) => OpenProjectsWindow();
|
||||
menu.Items.Add(showProjects);
|
||||
|
||||
menu.Items.Add(new Separator());
|
||||
|
||||
var exit = new MenuItem { Header = "Beenden" };
|
||||
exit.Click += (_, _) => ExitApp();
|
||||
menu.Items.Add(exit);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private void OpenChangesWindow()
|
||||
{
|
||||
// Fenster beim ersten Mal erstellen
|
||||
if (_pendingChangesWindow is null)
|
||||
{
|
||||
_pendingChangesViewModel = new PendingChangesViewModel(_apiClient!);
|
||||
_pendingChangesWindow = new PendingChangesWindow(_pendingChangesViewModel);
|
||||
}
|
||||
|
||||
// Fenster anzeigen/aktivieren
|
||||
_pendingChangesWindow.Show();
|
||||
_pendingChangesWindow.Activate();
|
||||
_ = _pendingChangesViewModel!.LoadProjectsCommand.ExecuteAsync(null);
|
||||
}
|
||||
|
||||
private void OpenProjectsWindow()
|
||||
{
|
||||
var existing = Windows.OfType<ProjectManagementWindow>().FirstOrDefault();
|
||||
if (existing is not null) { existing.Activate(); return; }
|
||||
|
||||
var vm = new ProjectManagementViewModel(_apiClient!);
|
||||
var window = new ProjectManagementWindow(vm);
|
||||
window.Show();
|
||||
_ = vm.LoadCommand.ExecuteAsync(null);
|
||||
}
|
||||
|
||||
private void OnChangeNotificationReceived(Guid projectId, string projectName, int count)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_trayIcon?.ShowNotification(
|
||||
title: "Neue Engineering-Daten",
|
||||
message: $"Projekt \"{projectName}\": {count} ausstehende Änderung(en).");
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPendingChangesWindowNotification(Guid projectId, string projectName, int count)
|
||||
{
|
||||
// Handler für PendingChangesWindow: nur aktualisieren wenn Fenster sichtbar ist
|
||||
if (_pendingChangesWindow?.IsVisible == true && _pendingChangesViewModel is not null)
|
||||
{
|
||||
Dispatcher.Invoke(() => _ = _pendingChangesViewModel.LoadChangesCommand.ExecuteAsync(null));
|
||||
}
|
||||
}
|
||||
|
||||
private async void ExitApp()
|
||||
{
|
||||
if (_signalR is not null)
|
||||
{
|
||||
await _signalR.DisposeAsync();
|
||||
_signalR = null;
|
||||
}
|
||||
_trayIcon?.Dispose();
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
protected override async void OnExit(ExitEventArgs e)
|
||||
{
|
||||
if (_signalR is not null)
|
||||
{
|
||||
await _signalR.DisposeAsync();
|
||||
_signalR = null;
|
||||
}
|
||||
_trayIcon?.Dispose();
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
10
EngineeringSync.TrayApp/AssemblyInfo.cs
Normal file
10
EngineeringSync.TrayApp/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
||||
BIN
EngineeringSync.TrayApp/Assets/tray.ico
Normal file
BIN
EngineeringSync.TrayApp/Assets/tray.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
32
EngineeringSync.TrayApp/Converters/Converters.cs
Normal file
32
EngineeringSync.TrayApp/Converters/Converters.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace EngineeringSync.TrayApp.Converters;
|
||||
|
||||
/// <summary>Gibt "Neues Projekt" oder "Projekt bearbeiten" zurück.</summary>
|
||||
public class NewEditHeaderConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
value is true ? "Neues Projekt anlegen" : "Projekt bearbeiten";
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>Gibt Visibility.Visible zurück wenn der String nicht leer ist.</summary>
|
||||
public class StringToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
string.IsNullOrEmpty(value as string) ? Visibility.Collapsed : Visibility.Visible;
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>Invertiert Boolean für Visibility (true = Collapsed, false = Visible).</summary>
|
||||
public class InverseBoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
value is true ? Visibility.Collapsed : Visibility.Visible;
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
27
EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
Normal file
27
EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EngineeringSync.Domain\EngineeringSync.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<!-- Kein Konsolenfenster im Hintergrund -->
|
||||
<ApplicationIcon>Assets\tray.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\tray.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
EngineeringSync.TrayApp/MainWindow.xaml
Normal file
12
EngineeringSync.TrayApp/MainWindow.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<Window x:Class="EngineeringSync.TrayApp.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.TrayApp"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
23
EngineeringSync.TrayApp/MainWindow.xaml.cs
Normal file
23
EngineeringSync.TrayApp/MainWindow.xaml.cs
Normal 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.TrayApp;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
59
EngineeringSync.TrayApp/Services/ApiClient.cs
Normal file
59
EngineeringSync.TrayApp/Services/ApiClient.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using EngineeringSync.Domain.Entities;
|
||||
|
||||
namespace EngineeringSync.TrayApp.Services;
|
||||
|
||||
public class ApiClient(HttpClient http)
|
||||
{
|
||||
private const string Base = "http://localhost:5050/api";
|
||||
|
||||
public Task<List<ProjectConfig>?> GetProjectsAsync() =>
|
||||
http.GetFromJsonAsync<List<ProjectConfig>>($"{Base}/projects");
|
||||
|
||||
public Task<List<PendingChange>?> GetChangesAsync(Guid projectId) =>
|
||||
http.GetFromJsonAsync<List<PendingChange>>($"{Base}/changes/{projectId}");
|
||||
|
||||
public Task<List<ScanResultDto>?> ScanFoldersAsync(string engineeringPath, string simulationPath, string fileExtensions) =>
|
||||
http.GetFromJsonAsync<List<ScanResultDto>>($"{Base}/projects/scan?engineeringPath={Uri.EscapeDataString(engineeringPath)}&simulationPath={Uri.EscapeDataString(simulationPath)}&fileExtensions={Uri.EscapeDataString(fileExtensions)}");
|
||||
|
||||
public async Task CreateProjectAsync(CreateProjectDto dto)
|
||||
{
|
||||
var resp = await http.PostAsJsonAsync($"{Base}/projects", dto);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task UpdateProjectAsync(Guid id, UpdateProjectDto dto)
|
||||
{
|
||||
var resp = await http.PutAsJsonAsync($"{Base}/projects/{id}", dto);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task DeleteProjectAsync(Guid id)
|
||||
{
|
||||
var resp = await http.DeleteAsync($"{Base}/projects/{id}");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task SyncChangesAsync(List<Guid> ids)
|
||||
{
|
||||
var resp = await http.PostAsJsonAsync($"{Base}/sync", new { ChangeIds = ids });
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task IgnoreChangesAsync(List<Guid> ids)
|
||||
{
|
||||
var resp = await http.PostAsJsonAsync($"{Base}/ignore", new { ChangeIds = ids });
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
public record CreateProjectDto(string Name, string EngineeringPath, string SimulationPath,
|
||||
string FileExtensions, bool IsActive = true, bool BackupEnabled = true,
|
||||
string? BackupPath = null, int MaxBackupsPerFile = 0);
|
||||
|
||||
public record UpdateProjectDto(string Name, string EngineeringPath, string SimulationPath,
|
||||
string FileExtensions, bool IsActive, bool BackupEnabled = true,
|
||||
string? BackupPath = null, int MaxBackupsPerFile = 0);
|
||||
|
||||
public record ScanResultDto(string RelativePath, string ChangeType, long Size, DateTime LastModified);
|
||||
57
EngineeringSync.TrayApp/Services/SignalRService.cs
Normal file
57
EngineeringSync.TrayApp/Services/SignalRService.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using EngineeringSync.Domain.Constants;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace EngineeringSync.TrayApp.Services;
|
||||
|
||||
public class SignalRService : IAsyncDisposable
|
||||
{
|
||||
private HubConnection? _connection;
|
||||
|
||||
public event Action<Guid, string, int>? ChangeNotificationReceived;
|
||||
public event Action? ProjectConfigChanged;
|
||||
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
_connection = new HubConnectionBuilder()
|
||||
.WithUrl("http://localhost:5050/notifications")
|
||||
.WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2),
|
||||
TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)])
|
||||
.Build();
|
||||
|
||||
// Register handlers on initial connection
|
||||
RegisterHandlers();
|
||||
|
||||
// Re-register handlers when reconnected after disconnection
|
||||
_connection.Reconnected += async (connectionId) =>
|
||||
{
|
||||
RegisterHandlers();
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
|
||||
await _connection.StartAsync(ct);
|
||||
}
|
||||
|
||||
private void RegisterHandlers()
|
||||
{
|
||||
if (_connection is null)
|
||||
return;
|
||||
|
||||
// Remove existing handlers to prevent duplicates
|
||||
_connection.Remove(HubMethodNames.ReceiveChangeNotification);
|
||||
_connection.Remove(HubMethodNames.ProjectConfigChanged);
|
||||
|
||||
// Register handlers
|
||||
_connection.On<Guid, string, int>(HubMethodNames.ReceiveChangeNotification,
|
||||
(projectId, projectName, count) =>
|
||||
ChangeNotificationReceived?.Invoke(projectId, projectName, count));
|
||||
|
||||
_connection.On(HubMethodNames.ProjectConfigChanged,
|
||||
() => ProjectConfigChanged?.Invoke());
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_connection is not null)
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
117
EngineeringSync.TrayApp/ViewModels/PendingChangesViewModel.cs
Normal file
117
EngineeringSync.TrayApp/ViewModels/PendingChangesViewModel.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.TrayApp.Services;
|
||||
|
||||
namespace EngineeringSync.TrayApp.ViewModels;
|
||||
|
||||
public partial class PendingChangesViewModel(ApiClient api) : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private ObservableCollection<ProjectConfig> _projects = [];
|
||||
[ObservableProperty] private ProjectConfig? _selectedProject;
|
||||
[ObservableProperty] private ObservableCollection<PendingChangeItem> _changes = [];
|
||||
[ObservableProperty] private bool _isLoading;
|
||||
[ObservableProperty] private string _statusMessage = string.Empty;
|
||||
|
||||
partial void OnSelectedProjectChanged(ProjectConfig? value)
|
||||
{
|
||||
if (value is not null)
|
||||
_ = LoadChangesAsync();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LoadProjectsAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
var projects = await api.GetProjectsAsync() ?? [];
|
||||
Projects = new ObservableCollection<ProjectConfig>(projects);
|
||||
if (SelectedProject is null && projects.Count > 0)
|
||||
SelectedProject = projects[0];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] LoadProjects Fehler: {ex}");
|
||||
StatusMessage = "Service nicht erreichbar.";
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LoadChangesAsync()
|
||||
{
|
||||
if (SelectedProject is null) return;
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
var changes = await api.GetChangesAsync(SelectedProject.Id) ?? [];
|
||||
Changes = new ObservableCollection<PendingChangeItem>(
|
||||
changes.Select(c => new PendingChangeItem(c)));
|
||||
StatusMessage = $"{Changes.Count} ausstehende Änderung(en)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] LoadChanges Fehler: {ex}");
|
||||
StatusMessage = "Fehler beim Laden.";
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SyncSelectedAsync()
|
||||
{
|
||||
var ids = Changes.Where(c => c.IsSelected).Select(c => c.Change.Id).ToList();
|
||||
if (ids.Count == 0) return;
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
await api.SyncChangesAsync(ids);
|
||||
await LoadChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] SyncSelected Fehler: {ex}");
|
||||
StatusMessage = "Sync fehlgeschlagen.";
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task IgnoreSelectedAsync()
|
||||
{
|
||||
var ids = Changes.Where(c => c.IsSelected).Select(c => c.Change.Id).ToList();
|
||||
if (ids.Count == 0) return;
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
await api.IgnoreChangesAsync(ids);
|
||||
await LoadChangesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] IgnoreSelected Fehler: {ex}");
|
||||
StatusMessage = "Ignore fehlgeschlagen.";
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void SelectAll() { foreach (var c in Changes) c.IsSelected = true; }
|
||||
|
||||
[RelayCommand]
|
||||
public void SelectNone() { foreach (var c in Changes) c.IsSelected = false; }
|
||||
}
|
||||
|
||||
public partial class PendingChangeItem(PendingChange change) : ObservableObject
|
||||
{
|
||||
public PendingChange Change { get; } = change;
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
|
||||
public string FileName => Path.GetFileName(Change.RelativePath);
|
||||
public string RelativePath => Change.RelativePath;
|
||||
public string ChangeTypeDisplay => Change.ChangeType.ToString();
|
||||
public string CreatedAtDisplay => Change.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm:ss");
|
||||
}
|
||||
183
EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs
Normal file
183
EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.TrayApp.Services;
|
||||
|
||||
namespace EngineeringSync.TrayApp.ViewModels;
|
||||
|
||||
public partial class ProjectManagementViewModel(ApiClient api) : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private ObservableCollection<ProjectConfig> _projects = [];
|
||||
[ObservableProperty] private ProjectConfig? _selectedProject;
|
||||
[ObservableProperty] private bool _isEditing;
|
||||
[ObservableProperty] private string _editName = string.Empty;
|
||||
[ObservableProperty] private string _editEngineeringPath = string.Empty;
|
||||
[ObservableProperty] private string _editSimulationPath = string.Empty;
|
||||
[ObservableProperty] private string _editFileExtensions = ".jt,.cojt,.xml";
|
||||
[ObservableProperty] private bool _editIsActive = true;
|
||||
[ObservableProperty] private bool _editBackupEnabled = true;
|
||||
[ObservableProperty] private bool _editBackupUseCustomPath = false;
|
||||
[ObservableProperty] private string _editBackupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _editMaxBackupsPerFile = 0;
|
||||
[ObservableProperty] private bool _isNewProject;
|
||||
[ObservableProperty] private string _statusMessage = string.Empty;
|
||||
[ObservableProperty] private string _scanStatus = string.Empty;
|
||||
[ObservableProperty] private ObservableCollection<ScanResultDto> _scanResults = [];
|
||||
public bool HasScanResults => ScanResults.Count > 0;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
var projects = await api.GetProjectsAsync() ?? [];
|
||||
Projects = new ObservableCollection<ProjectConfig>(projects);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void StartNewProject()
|
||||
{
|
||||
SelectedProject = null;
|
||||
IsNewProject = true;
|
||||
IsEditing = true;
|
||||
EditName = string.Empty;
|
||||
EditEngineeringPath = string.Empty;
|
||||
EditSimulationPath = string.Empty;
|
||||
EditFileExtensions = ".jt,.cojt,.xml";
|
||||
EditIsActive = true;
|
||||
EditBackupEnabled = true;
|
||||
EditBackupUseCustomPath = false;
|
||||
EditBackupCustomPath = string.Empty;
|
||||
EditMaxBackupsPerFile = 0;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void EditProject(ProjectConfig? project)
|
||||
{
|
||||
if (project is null)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
"Bitte zuerst ein Projekt auswählen.",
|
||||
"EngineeringSync",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
SelectedProject = project;
|
||||
IsNewProject = false;
|
||||
IsEditing = true;
|
||||
EditName = project.Name;
|
||||
EditEngineeringPath = project.EngineeringPath;
|
||||
EditSimulationPath = project.SimulationPath;
|
||||
EditFileExtensions = project.FileExtensions;
|
||||
EditIsActive = project.IsActive;
|
||||
EditBackupEnabled = project.BackupEnabled;
|
||||
EditBackupUseCustomPath = project.BackupPath is not null;
|
||||
EditBackupCustomPath = project.BackupPath ?? string.Empty;
|
||||
EditMaxBackupsPerFile = project.MaxBackupsPerFile;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EditName))
|
||||
{
|
||||
StatusMessage = "Name darf nicht leer sein.";
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
if (IsNewProject)
|
||||
await api.CreateProjectAsync(new CreateProjectDto(
|
||||
EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive,
|
||||
EditBackupEnabled,
|
||||
EditBackupUseCustomPath ? EditBackupCustomPath : null,
|
||||
EditMaxBackupsPerFile));
|
||||
else
|
||||
await api.UpdateProjectAsync(SelectedProject!.Id, new UpdateProjectDto(
|
||||
EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive,
|
||||
EditBackupEnabled,
|
||||
EditBackupUseCustomPath ? EditBackupCustomPath : null,
|
||||
EditMaxBackupsPerFile));
|
||||
|
||||
IsEditing = false;
|
||||
await LoadAsync();
|
||||
StatusMessage = IsNewProject ? "Projekt erstellt." : "Projekt aktualisiert.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] Save Fehler: {ex}");
|
||||
StatusMessage = $"Fehler: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void Cancel() => IsEditing = false;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task DeleteAsync(ProjectConfig? project)
|
||||
{
|
||||
if (project is null)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
"Bitte zuerst ein Projekt auswählen.",
|
||||
"EngineeringSync",
|
||||
System.Windows.MessageBoxButton.OK,
|
||||
System.Windows.MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await api.DeleteProjectAsync(project.Id);
|
||||
await LoadAsync();
|
||||
StatusMessage = "Projekt gelöscht.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TrayApp] Save Fehler: {ex}");
|
||||
StatusMessage = $"Fehler: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task ScanFoldersAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(EditEngineeringPath) || string.IsNullOrWhiteSpace(EditSimulationPath))
|
||||
{
|
||||
ScanStatus = "Bitte beide Ordnerpfade angeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!System.IO.Directory.Exists(EditEngineeringPath))
|
||||
{
|
||||
ScanStatus = $"Engineering-Ordner existiert nicht: {EditEngineeringPath}";
|
||||
return;
|
||||
}
|
||||
if (!System.IO.Directory.Exists(EditSimulationPath))
|
||||
{
|
||||
ScanStatus = $"Simulations-Ordner existiert nicht: {EditSimulationPath}";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ScanStatus = "Wird gescannt...";
|
||||
ScanResults.Clear();
|
||||
|
||||
var results = await api.ScanFoldersAsync(EditEngineeringPath, EditSimulationPath, EditFileExtensions);
|
||||
if (results is null || results.Count == 0)
|
||||
{
|
||||
ScanStatus = "Keine Unterschiede gefunden - Ordner sind synchron.";
|
||||
}
|
||||
else
|
||||
{
|
||||
ScanStatus = $"{results.Count} Unterschied(e) gefunden:";
|
||||
foreach (var r in results)
|
||||
ScanResults.Add(r);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ScanStatus = $"Scan-Fehler: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
67
EngineeringSync.TrayApp/Views/PendingChangesWindow.xaml
Normal file
67
EngineeringSync.TrayApp/Views/PendingChangesWindow.xaml
Normal file
@@ -0,0 +1,67 @@
|
||||
<Window x:Class="EngineeringSync.TrayApp.Views.PendingChangesWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Ausstehende Änderungen – EngineeringSync"
|
||||
Height="520" Width="860"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="#F5F5F5">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Projekt-Auswahl -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
|
||||
<TextBlock Text="Projekt:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/>
|
||||
<ComboBox ItemsSource="{Binding Projects}"
|
||||
SelectedItem="{Binding SelectedProject}"
|
||||
DisplayMemberPath="Name"
|
||||
Width="300" />
|
||||
<Button Content="Aktualisieren" Margin="8,0,0,0" Padding="10,4"
|
||||
Command="{Binding LoadChangesCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Änderungsliste -->
|
||||
<DataGrid Grid.Row="1"
|
||||
ItemsSource="{Binding Changes}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="Horizontal"
|
||||
Background="White"
|
||||
BorderBrush="#DDDDDD"
|
||||
BorderThickness="1">
|
||||
<DataGrid.Columns>
|
||||
<DataGridCheckBoxColumn Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="" Width="40" />
|
||||
<DataGridTextColumn Binding="{Binding FileName}" Header="Datei" Width="200" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Binding="{Binding RelativePath}" Header="Pfad" Width="*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Binding="{Binding ChangeTypeDisplay}" Header="Änderungstyp" Width="120" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Binding="{Binding CreatedAtDisplay}" Header="Zeitpunkt" Width="160" IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Auswahl-Buttons -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,4">
|
||||
<Button Content="Alle auswählen" Padding="8,4" Margin="0,0,4,0"
|
||||
Command="{Binding SelectAllCommand}" />
|
||||
<Button Content="Keine auswählen" Padding="8,4"
|
||||
Command="{Binding SelectNoneCommand}" />
|
||||
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center"
|
||||
Margin="16,0,0,0" Foreground="Gray" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Aktions-Buttons -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,4,0,0">
|
||||
<Button Content="Ausgewählte ignorieren" Padding="10,6" Margin="0,0,8,0"
|
||||
Command="{Binding IgnoreSelectedCommand}" />
|
||||
<Button Content="Ausgewählte synchronisieren" Padding="10,6"
|
||||
Background="#0078D4" Foreground="White" BorderThickness="0"
|
||||
Command="{Binding SyncSelectedCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
13
EngineeringSync.TrayApp/Views/PendingChangesWindow.xaml.cs
Normal file
13
EngineeringSync.TrayApp/Views/PendingChangesWindow.xaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Windows;
|
||||
using EngineeringSync.TrayApp.ViewModels;
|
||||
|
||||
namespace EngineeringSync.TrayApp.Views;
|
||||
|
||||
public partial class PendingChangesWindow : Window
|
||||
{
|
||||
public PendingChangesWindow(PendingChangesViewModel vm)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = vm;
|
||||
}
|
||||
}
|
||||
215
EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml
Normal file
215
EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml
Normal file
@@ -0,0 +1,215 @@
|
||||
<Window x:Class="EngineeringSync.TrayApp.Views.ProjectManagementWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:EngineeringSync.TrayApp.Converters"
|
||||
Title="Projektverwaltung – EngineeringSync"
|
||||
Height="800" Width="800"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="#F5F5F5">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
|
||||
<conv:NewEditHeaderConverter x:Key="NewEditHeaderConverter"/>
|
||||
<conv:StringToVisibilityConverter x:Key="StringToVisConverter"/>
|
||||
<conv:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisConverter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="350" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Linke Spalte: Projektliste -->
|
||||
<Grid Grid.Column="0" Margin="0,0,12,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Überwachte Projekte" FontWeight="Bold" FontSize="14" Margin="0,0,0,8"/>
|
||||
|
||||
<ListView Grid.Row="1" ItemsSource="{Binding Projects}"
|
||||
SelectedItem="{Binding SelectedProject}"
|
||||
Background="White" BorderBrush="#DDDDDD">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="4">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding EngineeringPath}" FontSize="11" Foreground="Gray" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Aktiv: " FontSize="11" Foreground="Gray"/>
|
||||
<TextBlock Text="{Binding IsActive}" FontSize="11" Foreground="Gray"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<Button Content="+ Neues Projekt" Padding="10,6" Margin="0,0,8,0"
|
||||
Command="{Binding StartNewProjectCommand}" />
|
||||
<Button Content="Bearbeiten" Padding="10,6" Margin="0,0,8,0"
|
||||
Command="{Binding EditProjectCommand}"
|
||||
CommandParameter="{Binding SelectedProject}" />
|
||||
<Button Content="Löschen" Padding="10,6" Foreground="Red"
|
||||
Command="{Binding DeleteCommand}"
|
||||
CommandParameter="{Binding SelectedProject}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Rechte Spalte: Editierformular -->
|
||||
<Border Grid.Column="1"
|
||||
Background="White"
|
||||
BorderBrush="#DDDDDD"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="16"
|
||||
Visibility="{Binding IsEditing, Converter={StaticResource BoolToVisConverter}}">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding IsNewProject, Converter={StaticResource NewEditHeaderConverter}}"
|
||||
FontWeight="Bold" FontSize="14" Margin="0,0,0,16"/>
|
||||
|
||||
<TextBlock Text="Name:" Margin="0,0,0,4" FontWeight="SemiBold"/>
|
||||
<TextBox Text="{Binding EditName, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0,0,0,12" Padding="6"/>
|
||||
|
||||
<TextBlock Text="Engineering-Pfad:" Margin="0,0,0,4" FontWeight="SemiBold"/>
|
||||
<DockPanel Margin="0,0,0,12">
|
||||
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
|
||||
Click="BrowseEngineering_Click"/>
|
||||
<TextBox Text="{Binding EditEngineeringPath, UpdateSourceTrigger=PropertyChanged}" Padding="6"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="Simulations-Pfad:" Margin="0,0,0,4" FontWeight="SemiBold"/>
|
||||
<DockPanel Margin="0,0,0,12">
|
||||
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
|
||||
Click="BrowseSimulation_Click"/>
|
||||
<TextBox Text="{Binding EditSimulationPath, UpdateSourceTrigger=PropertyChanged}" Padding="6"/>
|
||||
</DockPanel>
|
||||
|
||||
<TextBlock Text="Dateiendungen (komma-getrennt):" Margin="0,0,0,4" FontWeight="SemiBold"/>
|
||||
<TextBox Text="{Binding EditFileExtensions, UpdateSourceTrigger=PropertyChanged}"
|
||||
Margin="0,0,0,12" Padding="6">
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None">
|
||||
<VisualBrush.Visual>
|
||||
<TextBlock Text="z.B. .jt,.cojt (leer = alle Dateien)" Foreground="#999999" Margin="6,0" FontSize="11"/>
|
||||
</VisualBrush.Visual>
|
||||
</VisualBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<CheckBox Content="Aktiv (Watcher läuft)"
|
||||
IsChecked="{Binding EditIsActive}"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<!-- ORDNER-PRÜFUNG -->
|
||||
<Border Background="#E3F2FD" BorderBrush="#90CAF9" BorderThickness="1"
|
||||
CornerRadius="4" Padding="12" Margin="0,0,0,16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Unterschiede prüfen" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||
<TextBlock Text="Prüft beide Ordner und zeigt Unterschiede vor dem Speichern."
|
||||
FontSize="11" Foreground="#555555" Margin="0,0,0,8" TextWrapping="Wrap"/>
|
||||
<Button Content="Ordner prüfen..." Padding="12,6"
|
||||
Command="{Binding ScanFoldersCommand}"
|
||||
HorizontalAlignment="Left"/>
|
||||
<TextBlock Text="{Binding ScanStatus}" FontSize="11" Foreground="#1976D2"
|
||||
Margin="0,4,0,0" TextWrapping="Wrap"
|
||||
Visibility="{Binding ScanStatus, Converter={StaticResource StringToVisConverter}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Scan-Ergebnisse -->
|
||||
<ItemsControl ItemsSource="{Binding ScanResults}" Margin="0,0,0,16"
|
||||
Visibility="{Binding HasScanResults, Converter={StaticResource BoolToVisConverter}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="#FFF8E1" BorderBrush="#FFD54F" BorderThickness="1"
|
||||
CornerRadius="4" Padding="8" Margin="0,4">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding ChangeType}" FontWeight="SemiBold"
|
||||
Foreground="#F57C00" Width="70"/>
|
||||
<TextBlock Text="{Binding RelativePath}" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<TextBlock FontSize="11" Foreground="#666666">
|
||||
<Run Text="{Binding Size, StringFormat='{}{0:N0} Bytes'}"/>
|
||||
<Run Text=" • "/>
|
||||
<Run Text="{Binding LastModified, StringFormat='yyyy-MM-dd HH:mm'}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- BACKUP-Sektion -->
|
||||
<Separator Margin="0,8,0,12"/>
|
||||
<TextBlock Text="BACKUP" FontSize="10" FontWeight="Bold" Foreground="#888888"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<CheckBox Content="Backups vor dem Überschreiben erstellen"
|
||||
IsChecked="{Binding EditBackupEnabled}"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<StackPanel Visibility="{Binding EditBackupEnabled,
|
||||
Converter={StaticResource BoolToVisConverter}}">
|
||||
|
||||
<TextBlock Text="Speicherort:" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||
<!-- Mode=OneWay: gegenseitiger Ausschluss läuft über GroupName + TwoWay am zweiten Radio -->
|
||||
<RadioButton GroupName="BackupLocation" Content="Gleicher Ordner wie Simulationsdatei"
|
||||
IsChecked="{Binding EditBackupUseCustomPath,
|
||||
Converter={StaticResource InverseBoolToVisConverter},
|
||||
Mode=OneWay}"
|
||||
Margin="0,0,0,4"/>
|
||||
<RadioButton GroupName="BackupLocation" Content="Eigener Backup-Ordner"
|
||||
IsChecked="{Binding EditBackupUseCustomPath, Mode=TwoWay}"
|
||||
Margin="0,0,0,6"/>
|
||||
|
||||
<DockPanel Margin="0,0,0,10"
|
||||
Visibility="{Binding EditBackupUseCustomPath,
|
||||
Converter={StaticResource BoolToVisConverter}}">
|
||||
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
|
||||
Click="BrowseBackup_Click"/>
|
||||
<TextBox Text="{Binding EditBackupCustomPath, UpdateSourceTrigger=PropertyChanged}"
|
||||
Padding="6"/>
|
||||
</DockPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="Max. Backups pro Datei:" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBox Width="50" Padding="4,2"
|
||||
Text="{Binding EditMaxBackupsPerFile, UpdateSourceTrigger=PropertyChanged}"
|
||||
VerticalContentAlignment="Center"/>
|
||||
<TextBlock Text="(0 = unbegrenzt)" FontSize="11" Foreground="Gray"
|
||||
VerticalAlignment="Center" Margin="6,0,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="{Binding StatusMessage}" Foreground="OrangeRed"
|
||||
TextWrapping="Wrap" Margin="0,0,0,8"
|
||||
Visibility="{Binding StatusMessage, Converter={StaticResource StringToVisConverter}}"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="Abbrechen" Padding="10,6" Margin="0,0,8,0"
|
||||
Command="{Binding CancelCommand}" />
|
||||
<Button Content="Speichern" Padding="10,6"
|
||||
Background="#0078D4" Foreground="White" BorderThickness="0"
|
||||
Command="{Binding SaveCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Windows;
|
||||
using EngineeringSync.TrayApp.ViewModels;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EngineeringSync.TrayApp.Views;
|
||||
|
||||
public partial class ProjectManagementWindow : Window
|
||||
{
|
||||
private readonly ProjectManagementViewModel _vm;
|
||||
|
||||
public ProjectManagementWindow(ProjectManagementViewModel vm)
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = vm;
|
||||
DataContext = vm;
|
||||
}
|
||||
|
||||
private void BrowseEngineering_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new OpenFolderDialog
|
||||
{
|
||||
Title = "Engineering-Quellpfad wählen",
|
||||
InitialDirectory = _vm.EditEngineeringPath
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
_vm.EditEngineeringPath = dialog.FolderName;
|
||||
}
|
||||
|
||||
private void BrowseSimulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new OpenFolderDialog
|
||||
{
|
||||
Title = "Simulations-Zielpfad wählen",
|
||||
InitialDirectory = _vm.EditSimulationPath
|
||||
};
|
||||
if (dialog.ShowDialog() == true)
|
||||
_vm.EditSimulationPath = dialog.FolderName;
|
||||
}
|
||||
|
||||
private void BrowseBackup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFolderDialog
|
||||
{
|
||||
Title = "Backup-Verzeichnis wählen",
|
||||
InitialDirectory = string.IsNullOrEmpty(_vm.EditBackupCustomPath)
|
||||
? null
|
||||
: _vm.EditBackupCustomPath
|
||||
};
|
||||
if (dlg.ShowDialog() == true)
|
||||
_vm.EditBackupCustomPath = dlg.FolderName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user