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:
22
.gitattributes
vendored
Normal file
22
.gitattributes
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Zeilenenden normalisieren
|
||||
* text=auto
|
||||
|
||||
# .NET-spezifisch
|
||||
*.cs text eol=crlf
|
||||
*.xaml text eol=crlf
|
||||
*.csproj text eol=crlf
|
||||
*.slnx text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.targets text eol=crlf
|
||||
*.config text eol=crlf
|
||||
*.json text eol=crlf
|
||||
*.md text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
*.iss text eol=crlf
|
||||
|
||||
# Binärdateien
|
||||
*.png binary
|
||||
*.ico binary
|
||||
*.exe binary
|
||||
*.dll binary
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Build-Ausgaben
|
||||
bin/
|
||||
obj/
|
||||
dist/
|
||||
|
||||
# Heruntergeladene Redistributables (werden beim Build automatisch geladen)
|
||||
installer/redist/
|
||||
|
||||
# User-spezifische IDE-Dateien
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
|
||||
# dotnet tools
|
||||
.dotnet/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
firebase-debug.log
|
||||
|
||||
# Serena / Claude intern
|
||||
.serena/
|
||||
.claude/settings.local.json
|
||||
155
AGENTS.md
Normal file
155
AGENTS.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Build, Test & Run Commands
|
||||
|
||||
```bash
|
||||
# Build entire solution
|
||||
dotnet build EngineeringSync.slnx
|
||||
|
||||
# Run the background service (dev mode)
|
||||
dotnet run --project EngineeringSync.Service
|
||||
|
||||
# Run the WPF tray app
|
||||
dotnet run --project EngineeringSync.TrayApp
|
||||
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run a single test project
|
||||
dotnet test EngineeringSync.Tests --filter "FullyQualifiedName~WatcherServiceTests"
|
||||
|
||||
# Run tests with verbose output
|
||||
dotnet test --verbosity normal
|
||||
|
||||
# Build Release
|
||||
dotnet build EngineeringSync.slnx -c Release
|
||||
|
||||
# Install as Windows Service (production)
|
||||
sc create EngineeringSync binPath="<path>\EngineeringSync.Service.exe"
|
||||
|
||||
# Build installer (requires Inno Setup 6)
|
||||
.\installer\build-installer.ps1
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### General Principles
|
||||
- Use C# 12+ features (file-scoped namespaces, collection expressions, primary constructors)
|
||||
- Use `[ObservableProperty]` and `[RelayCommand]` attributes from CommunityToolkit.Mvvm
|
||||
- Prefer source generators over boilerplate code
|
||||
|
||||
### Imports
|
||||
- Sort by: System namespaces first, then Microsoft, then third-party, then project-specific
|
||||
- Use file-scoped namespaces: `namespace EngineeringSync.Domain.Entities;`
|
||||
- Global using statements in each project for common types
|
||||
|
||||
### Types & Collections
|
||||
- Use collection expressions `[]` instead of `new List<T>()` where possible
|
||||
- Use `IReadOnlyList<T>` for parameters that should not be modified
|
||||
- Use `Guid` for all ID types
|
||||
- Use `DateTime.UtcNow` for timestamps, convert to local time in UI layer only
|
||||
|
||||
### Naming Conventions
|
||||
- **Classes/Interfaces:** PascalCase (e.g., `WatcherService`, `IApiClient`)
|
||||
- **Methods:** PascalCase (e.g., `StartWatchingAsync`)
|
||||
- **Private fields:** camelCase (e.g., `_watchers`, `_channel`)
|
||||
- **Properties:** PascalCase (e.g., `IsActive`, `EngineeringPath`)
|
||||
- **Parameters:** camelCase (e.g., `project`, `stoppingToken`)
|
||||
- **Constants:** PascalCase (e.g., `DefaultTimeout`)
|
||||
|
||||
### Records vs Classes
|
||||
- Use `record` for DTOs and API models
|
||||
- Use `class` for entities with mutable properties
|
||||
|
||||
### Error Handling
|
||||
- Use `try/catch` with specific exception types
|
||||
- Log exceptions with context using `ILogger` (use structured logging: `logger.LogError(ex, "Message {Param}", param)`)
|
||||
- In ViewModels, catch exceptions and set `StatusMessage` for user feedback
|
||||
|
||||
### Async/Await
|
||||
- Always use `Async` suffix for async methods
|
||||
- Pass `CancellationToken` to all cancellable operations
|
||||
- Use `await using` for `IDisposable` resources that implement `IAsyncDisposable`
|
||||
|
||||
### Entity Framework Core
|
||||
- Use `IDbContextFactory<AppDbContext>` for scoped DbContext in background services
|
||||
- Use `await using var db = await dbFactory.CreateDbContextAsync(ct);`
|
||||
- Configure SQLite with WAL mode for better concurrency
|
||||
|
||||
### WPF / MVVM
|
||||
- ViewModels inherit from `ObservableObject` (CommunityToolkit.Mvvm)
|
||||
- Use `[ObservableProperty]` for properties that need change notification
|
||||
- Use `[RelayCommand]` for commands
|
||||
- Use `partial class` for generated properties
|
||||
|
||||
### Dependency Injection
|
||||
- Constructor injection with primary constructors:
|
||||
```csharp
|
||||
public sealed class WatcherService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IHubContext<NotificationHub> hub,
|
||||
ILogger<WatcherService> logger) : BackgroundService
|
||||
```
|
||||
|
||||
### File Organization
|
||||
```
|
||||
EngineeringSync.Domain/ - Entities, Enums, Interfaces
|
||||
EngineeringSync.Infrastructure/ - EF Core, DbContext, Migrations
|
||||
EngineeringSync.Service/ - API, Hubs, Background Services
|
||||
EngineeringSync.TrayApp/ - WPF Views, ViewModels, Services
|
||||
EngineeringSync.Setup/ - Installer wizard
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**FileSystemWatcher Debouncing:** Events flow into a `Channel<FileEvent>`, consumer groups by `(ProjectId, RelativePath)` within 2000ms window, SHA-256 confirms actual changes.
|
||||
|
||||
**SignalR Notifications:** `NotificationHub` at `/notifications` broadcasts `ReceiveChangeNotification(projectId, projectName, count)` to all clients.
|
||||
|
||||
**Backup before sync:** Use timestamped naming `{filename}_{yyyyMMdd_HHmmss}.bak`.
|
||||
|
||||
### Service API Endpoints
|
||||
- `GET/POST/PUT/DELETE /api/projects` - Project CRUD
|
||||
- `GET /api/changes/{projectId}` - Get pending changes
|
||||
- `GET /api/changes/{projectId}/history` - Get synced/ignored changes
|
||||
- `POST /api/sync` - Sync selected changes
|
||||
- `POST /api/ignore` - Ignore selected changes
|
||||
|
||||
### Database
|
||||
- SQLite with WAL mode
|
||||
- Entities: `ProjectConfig`, `FileRevision`, `PendingChange`
|
||||
- `ProjectConfig.FileExtensions` is comma-separated (e.g., ".jt,.cojt,.xml")
|
||||
|
||||
## Solution Structure
|
||||
|
||||
```
|
||||
EngineeringSync.slnx
|
||||
├── EngineeringSync.Domain/ (Class Library, net10.0) - Entities, Enums, Interfaces
|
||||
├── EngineeringSync.Infrastructure/ (Class Library, net10.0) - EF Core, DbContext
|
||||
├── EngineeringSync.Service/ (Worker Service, net10.0) - Kestrel on :5050, SignalR hub
|
||||
├── EngineeringSync.TrayApp/ (WPF App, net10.0-windows) - System tray + UI windows
|
||||
├── EngineeringSync.Setup/ (WPF App, net10.0-windows) - Setup wizard + Installer
|
||||
└── installer/
|
||||
├── setup.iss - Inno Setup Script
|
||||
└── build-installer.ps1 - Build + Publish + ISCC Pipeline
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** .NET 10
|
||||
- **Service:** Worker Service (Windows Service) + ASP.NET Core Minimal API + SignalR
|
||||
- **Client:** WPF (.NET 10-Windows) with `H.NotifyIcon.Wpf` + `CommunityToolkit.Mvvm`
|
||||
- **Database:** SQLite via EF Core 10 (WAL mode)
|
||||
- **File Watching:** `System.IO.FileSystemWatcher` + `System.Threading.Channels` (debouncing)
|
||||
|
||||
## Key Architecture
|
||||
|
||||
- **ProjectConfig in DB:** Managed via TrayApp UI, stored in SQLite. User adds/edits/deletes projects with folder browser dialogs. CRUD operations go through Service API, which dynamically starts/stops watchers.
|
||||
- **FileSystemWatcher Debouncing:** Events (Created, Changed, Renamed, Deleted) pushed into `Channel<FileEvent>`. Consumer groups events by `(ProjectId, RelativePath)` within 2000ms sliding window. SHA-256 hashing against `FileRevision` confirms actual changes before writing a `PendingChange`.
|
||||
- **SignalR Hub:** `NotificationHub` at `/notifications` broadcasts `ReceiveChangeNotification(projectId, projectName, count)` and `ProjectConfigChanged()`.
|
||||
|
||||
## Notes
|
||||
- Domain project has no dependencies (pure entities/enums)
|
||||
- Infrastructure depends on Domain
|
||||
- Service depends on Domain + Infrastructure
|
||||
- TrayApp depends on Domain + Service (via HTTP client)
|
||||
90
CLAUDE.md
Normal file
90
CLAUDE.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**EngineeringSync** – A .NET 10 middleware tool that bridges mechanical engineering (CAD) and simulation (Process Simulate). A Windows Service watches an engineering folder for file changes, records them in SQLite, and notifies a WPF System Tray App via SignalR. The simulation engineer reviews changes and triggers a controlled sync to avoid data corruption.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** .NET 10
|
||||
- **Service:** Worker Service (Windows Service) + ASP.NET Core Minimal API + SignalR
|
||||
- **Client:** WPF (.NET 10-Windows) with `H.NotifyIcon.Wpf` + `CommunityToolkit.Mvvm`
|
||||
- **Database:** SQLite via EF Core 10 (WAL mode)
|
||||
- **File Watching:** `System.IO.FileSystemWatcher` + `System.Threading.Channels` (debouncing)
|
||||
|
||||
## Solution Structure
|
||||
|
||||
```
|
||||
EngineeringSync.slnx
|
||||
├── EngineeringSync.Domain (Class Library, net10.0) – Entities, Enums, Interfaces
|
||||
├── EngineeringSync.Infrastructure (Class Library, net10.0) – EF Core, AppDbContext
|
||||
├── EngineeringSync.Service (Worker Service, net10.0) – Kestrel on :5050, SignalR hub
|
||||
├── EngineeringSync.TrayApp (WPF App, net10.0-windows) – System tray + UI windows
|
||||
└── EngineeringSync.Setup (WPF App, net10.0-windows) – Setup-Wizard + Installer
|
||||
installer/
|
||||
├── setup.iss – Inno Setup Script
|
||||
└── build-installer.ps1 – Build + Publish + ISCC Pipeline
|
||||
```
|
||||
|
||||
**Dependency direction (strict one-way):**
|
||||
`TrayApp` → `Domain` | `Service` → `Domain` + `Infrastructure` | `Infrastructure` → `Domain`
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build entire solution
|
||||
dotnet build EngineeringSync.slnx
|
||||
|
||||
# Installer bauen (benötigt Inno Setup 6 installiert)
|
||||
.\installer\build-installer.ps1
|
||||
|
||||
# Run the background service (dev mode)
|
||||
dotnet run --project EngineeringSync.Service
|
||||
|
||||
# Run the WPF tray app
|
||||
dotnet run --project EngineeringSync.TrayApp
|
||||
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run a single test project
|
||||
dotnet test EngineeringSync.Tests --filter "FullyQualifiedName~WatcherServiceTests"
|
||||
|
||||
# Install as Windows Service (production)
|
||||
sc create EngineeringSync binPath="<path>\EngineeringSync.Service.exe"
|
||||
```
|
||||
|
||||
## Key Architecture Decisions
|
||||
|
||||
### ProjectConfig in DB (not appsettings.json)
|
||||
`ProjectConfig` is managed via the TrayApp UI and stored in SQLite. The user can add/edit/delete projects with folder browser dialogs. CRUD operations go through the Service API, which dynamically starts/stops watchers.
|
||||
|
||||
### FileSystemWatcher Debouncing
|
||||
Events (Created, Changed, Renamed, Deleted) are pushed into a `Channel<FileEvent>`. A consumer groups events by `(ProjectId, RelativePath)` within a 2000ms sliding window. SHA-256 hashing against `FileRevision` confirms actual changes before writing a `PendingChange`.
|
||||
|
||||
### Service API (Kestrel on localhost:5050)
|
||||
- **Project CRUD:** `GET/POST/PUT/DELETE /api/projects`
|
||||
- **Changes:** `GET /api/changes/{projectId}`, `GET /api/changes/{projectId}/history`
|
||||
- **Actions:** `POST /api/sync`, `POST /api/ignore`
|
||||
|
||||
Backup before overwrite uses timestamped naming: `{filename}_{yyyyMMdd_HHmmss}.bak`
|
||||
|
||||
### SignalR Hub (`NotificationHub` at `/notifications`)
|
||||
- `ReceiveChangeNotification(projectId, projectName, count)` – New pending changes
|
||||
- `ProjectConfigChanged()` – Project CRUD happened
|
||||
|
||||
### TrayApp Windows
|
||||
- **ProjectManagementWindow** – CRUD for projects with `FolderBrowserDialog` for path selection
|
||||
- **PendingChangesWindow** – DataGrid with project selector, sync/ignore actions, auto-refresh via SignalR
|
||||
|
||||
All ViewModels use `CommunityToolkit.Mvvm` source generators (`[ObservableProperty]`, `[RelayCommand]`).
|
||||
|
||||
## Domain Entities
|
||||
|
||||
| Entity | Key Fields |
|
||||
|---|---|
|
||||
| `ProjectConfig` | `Id` (Guid), `Name`, `EngineeringPath`, `SimulationPath`, `FileExtensions`, `IsActive`, `CreatedAt` |
|
||||
| `FileRevision` | `Id` (Guid), `ProjectId` (FK), `RelativePath`, `FileHash`, `Size`, `LastModified` |
|
||||
| `PendingChange` | `Id` (Guid), `ProjectId` (FK), `RelativePath`, `ChangeType` (Created/Modified/Renamed/Deleted), `OldRelativePath?`, `Status` (Pending/Synced/Ignored), `CreatedAt`, `SyncedAt?` |
|
||||
7
EngineeringSync.Domain/Constants/HubMethodNames.cs
Normal file
7
EngineeringSync.Domain/Constants/HubMethodNames.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace EngineeringSync.Domain.Constants;
|
||||
|
||||
public static class HubMethodNames
|
||||
{
|
||||
public const string ReceiveChangeNotification = "ReceiveChangeNotification";
|
||||
public const string ProjectConfigChanged = "ProjectConfigChanged";
|
||||
}
|
||||
9
EngineeringSync.Domain/EngineeringSync.Domain.csproj
Normal file
9
EngineeringSync.Domain/EngineeringSync.Domain.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
12
EngineeringSync.Domain/Entities/FileRevision.cs
Normal file
12
EngineeringSync.Domain/Entities/FileRevision.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace EngineeringSync.Domain.Entities;
|
||||
|
||||
public class FileRevision
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid ProjectId { get; set; }
|
||||
public ProjectConfig Project { get; set; } = null!;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public string FileHash { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
14
EngineeringSync.Domain/Entities/PendingChange.cs
Normal file
14
EngineeringSync.Domain/Entities/PendingChange.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace EngineeringSync.Domain.Entities;
|
||||
|
||||
public class PendingChange
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid ProjectId { get; set; }
|
||||
public ProjectConfig Project { get; set; } = null!;
|
||||
public string RelativePath { get; set; } = string.Empty;
|
||||
public ChangeType ChangeType { get; set; }
|
||||
public string? OldRelativePath { get; set; }
|
||||
public ChangeStatus Status { get; set; } = ChangeStatus.Pending;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? SyncedAt { get; set; }
|
||||
}
|
||||
28
EngineeringSync.Domain/Entities/ProjectConfig.cs
Normal file
28
EngineeringSync.Domain/Entities/ProjectConfig.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace EngineeringSync.Domain.Entities;
|
||||
|
||||
public class ProjectConfig
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string EngineeringPath { get; set; } = string.Empty;
|
||||
public string SimulationPath { get; set; } = string.Empty;
|
||||
/// <summary>Komma-separiert, z.B. ".jt,.cojt,.xml"</summary>
|
||||
public string FileExtensions { get; set; } = string.Empty;
|
||||
public bool IsActive { get; set; } = true;
|
||||
public bool BackupEnabled { get; set; } = true;
|
||||
public string? BackupPath { get; set; } = null;
|
||||
public int MaxBackupsPerFile { get; set; } = 0;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<FileRevision> FileRevisions { get; set; } = [];
|
||||
public ICollection<PendingChange> PendingChanges { get; set; } = [];
|
||||
|
||||
public IEnumerable<string> GetExtensions()
|
||||
{
|
||||
// Wenn leer oder "*" → alle Dateien beobachten
|
||||
if (string.IsNullOrWhiteSpace(FileExtensions) || FileExtensions.Trim() == "*")
|
||||
return ["*"];
|
||||
|
||||
return FileExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
}
|
||||
8
EngineeringSync.Domain/Enums/ChangeStatus.cs
Normal file
8
EngineeringSync.Domain/Enums/ChangeStatus.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace EngineeringSync.Domain.Entities;
|
||||
|
||||
public enum ChangeStatus
|
||||
{
|
||||
Pending,
|
||||
Synced,
|
||||
Ignored
|
||||
}
|
||||
9
EngineeringSync.Domain/Enums/ChangeType.cs
Normal file
9
EngineeringSync.Domain/Enums/ChangeType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace EngineeringSync.Domain.Entities;
|
||||
|
||||
public enum ChangeType
|
||||
{
|
||||
Created,
|
||||
Modified,
|
||||
Renamed,
|
||||
Deleted
|
||||
}
|
||||
52
EngineeringSync.Infrastructure/AppDbContext.cs
Normal file
52
EngineeringSync.Infrastructure/AppDbContext.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EngineeringSync.Infrastructure;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (optionsBuilder.IsConfigured) return;
|
||||
// WAL-Modus für bessere Nebenläufigkeit zwischen Watcher-Writer und API-Reader
|
||||
optionsBuilder.UseSqlite(o => o.CommandTimeout(30));
|
||||
}
|
||||
|
||||
|
||||
public DbSet<ProjectConfig> Projects => Set<ProjectConfig>();
|
||||
public DbSet<FileRevision> FileRevisions => Set<FileRevision>();
|
||||
public DbSet<PendingChange> PendingChanges => Set<PendingChange>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ProjectConfig>(e =>
|
||||
{
|
||||
e.HasKey(p => p.Id);
|
||||
e.Property(p => p.Name).IsRequired().HasMaxLength(200);
|
||||
e.Property(p => p.EngineeringPath).IsRequired();
|
||||
e.Property(p => p.SimulationPath).IsRequired();
|
||||
e.Property(p => p.BackupEnabled).HasDefaultValue(true);
|
||||
e.Property(p => p.BackupPath).HasDefaultValue(null);
|
||||
e.Property(p => p.MaxBackupsPerFile).HasDefaultValue(0);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<FileRevision>(e =>
|
||||
{
|
||||
e.HasKey(r => r.Id);
|
||||
e.HasIndex(r => new { r.ProjectId, r.RelativePath }).IsUnique();
|
||||
e.HasOne(r => r.Project)
|
||||
.WithMany(p => p.FileRevisions)
|
||||
.HasForeignKey(r => r.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PendingChange>(e =>
|
||||
{
|
||||
e.HasKey(c => c.Id);
|
||||
e.HasOne(c => c.Project)
|
||||
.WithMany(p => p.PendingChanges)
|
||||
.HasForeignKey(c => c.ProjectId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EngineeringSync.Domain\EngineeringSync.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
154
EngineeringSync.Infrastructure/Migrations/20260325090857_InitialCreate.Designer.cs
generated
Normal file
154
EngineeringSync.Infrastructure/Migrations/20260325090857_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,154 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EngineeringSync.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260325090857_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId", "RelativePath")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FileRevisions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChangeType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OldRelativePath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("PendingChanges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EngineeringPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileExtensions")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SimulationPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("FileRevisions")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("PendingChanges")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Navigation("FileRevisions");
|
||||
|
||||
b.Navigation("PendingChanges");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EngineeringSync.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Projects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
EngineeringPath = table.Column<string>(type: "TEXT", nullable: false),
|
||||
SimulationPath = table.Column<string>(type: "TEXT", nullable: false),
|
||||
FileExtensions = table.Column<string>(type: "TEXT", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FileRevisions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
RelativePath = table.Column<string>(type: "TEXT", nullable: false),
|
||||
FileHash = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Size = table.Column<long>(type: "INTEGER", nullable: false),
|
||||
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FileRevisions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FileRevisions_Projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PendingChanges",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
RelativePath = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ChangeType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OldRelativePath = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
SyncedAt = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PendingChanges", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PendingChanges_Projects_ProjectId",
|
||||
column: x => x.ProjectId,
|
||||
principalTable: "Projects",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FileRevisions_ProjectId_RelativePath",
|
||||
table: "FileRevisions",
|
||||
columns: new[] { "ProjectId", "RelativePath" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PendingChanges_ProjectId",
|
||||
table: "PendingChanges",
|
||||
column: "ProjectId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FileRevisions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PendingChanges");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Projects");
|
||||
}
|
||||
}
|
||||
}
|
||||
167
EngineeringSync.Infrastructure/Migrations/20260326161840_AddBackupSettingsToProjectConfig.Designer.cs
generated
Normal file
167
EngineeringSync.Infrastructure/Migrations/20260326161840_AddBackupSettingsToProjectConfig.Designer.cs
generated
Normal file
@@ -0,0 +1,167 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EngineeringSync.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260326161840_AddBackupSettingsToProjectConfig")]
|
||||
partial class AddBackupSettingsToProjectConfig
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId", "RelativePath")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FileRevisions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChangeType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OldRelativePath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("PendingChanges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("BackupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("BackupPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EngineeringPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileExtensions")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxBackupsPerFile")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SimulationPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("FileRevisions")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("PendingChanges")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Navigation("FileRevisions");
|
||||
|
||||
b.Navigation("PendingChanges");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EngineeringSync.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBackupSettingsToProjectConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "BackupEnabled",
|
||||
table: "Projects",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BackupPath",
|
||||
table: "Projects",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxBackupsPerFile",
|
||||
table: "Projects",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BackupEnabled",
|
||||
table: "Projects");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BackupPath",
|
||||
table: "Projects");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxBackupsPerFile",
|
||||
table: "Projects");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EngineeringSync.Infrastructure.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId", "RelativePath")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("FileRevisions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChangeType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OldRelativePath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RelativePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("SyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.ToTable("PendingChanges");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("BackupEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("BackupPath")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EngineeringPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FileExtensions")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxBackupsPerFile")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SimulationPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Projects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.FileRevision", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("FileRevisions")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.PendingChange", b =>
|
||||
{
|
||||
b.HasOne("EngineeringSync.Domain.Entities.ProjectConfig", "Project")
|
||||
.WithMany("PendingChanges")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EngineeringSync.Domain.Entities.ProjectConfig", b =>
|
||||
{
|
||||
b.Navigation("FileRevisions");
|
||||
|
||||
b.Navigation("PendingChanges");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace EngineeringSync.Infrastructure;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, string dbPath)
|
||||
{
|
||||
// WAL=Write-Ahead Logging: ermöglicht gleichzeitiges Lesen (API) und Schreiben (Watcher)
|
||||
var connectionString = $"Data Source={dbPath};Mode=ReadWriteCreate;Cache=Shared";
|
||||
services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite(connectionString));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
52
EngineeringSync.Service/Api/ChangesApi.cs
Normal file
52
EngineeringSync.Service/Api/ChangesApi.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using EngineeringSync.Service.Models;
|
||||
using EngineeringSync.Service.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EngineeringSync.Service.Api;
|
||||
|
||||
public static class ChangesApi
|
||||
{
|
||||
public static IEndpointRouteBuilder MapChangesApi(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet("/api/changes/{projectId:guid}", async (Guid projectId, IDbContextFactory<AppDbContext> dbFactory,
|
||||
int page = 0, int pageSize = 100) =>
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var changes = await db.PendingChanges
|
||||
.Where(c => c.ProjectId == projectId && c.Status == ChangeStatus.Pending)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Skip(page * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
return Results.Ok(changes);
|
||||
});
|
||||
|
||||
app.MapGet("/api/changes/{projectId:guid}/history", async (Guid projectId,
|
||||
IDbContextFactory<AppDbContext> dbFactory) =>
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var changes = await db.PendingChanges
|
||||
.Where(c => c.ProjectId == projectId && c.Status != ChangeStatus.Pending)
|
||||
.OrderByDescending(c => c.SyncedAt ?? c.CreatedAt)
|
||||
.Take(100)
|
||||
.ToListAsync();
|
||||
return Results.Ok(changes);
|
||||
});
|
||||
|
||||
app.MapPost("/api/sync", async (SyncRequest req, SyncManager syncManager) =>
|
||||
{
|
||||
var result = await syncManager.SyncAsync(req.ChangeIds);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapPost("/api/ignore", async (IgnoreRequest req, SyncManager syncManager) =>
|
||||
{
|
||||
await syncManager.IgnoreAsync(req.ChangeIds);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
173
EngineeringSync.Service/Api/ProjectsApi.cs
Normal file
173
EngineeringSync.Service/Api/ProjectsApi.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using EngineeringSync.Domain.Constants;
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using EngineeringSync.Service.Hubs;
|
||||
using EngineeringSync.Service.Models;
|
||||
using EngineeringSync.Service.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EngineeringSync.Service.Api;
|
||||
|
||||
public static class ProjectsApi
|
||||
{
|
||||
public static IEndpointRouteBuilder MapProjectsApi(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/projects");
|
||||
|
||||
group.MapGet("/", async (IDbContextFactory<AppDbContext> dbFactory) =>
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var projects = await db.Projects.OrderBy(p => p.Name).ToListAsync();
|
||||
return Results.Ok(projects);
|
||||
});
|
||||
|
||||
group.MapGet("/{id:guid}", async (Guid id, IDbContextFactory<AppDbContext> dbFactory) =>
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var project = await db.Projects.FindAsync(id);
|
||||
return project is null ? Results.NotFound() : Results.Ok(project);
|
||||
});
|
||||
|
||||
group.MapPost("/", async (CreateProjectRequest req, IDbContextFactory<AppDbContext> dbFactory,
|
||||
WatcherService watcher, IHubContext<NotificationHub> hubContext, ILoggerFactory loggerFactory) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("ProjectsApi");
|
||||
if (!Directory.Exists(req.EngineeringPath))
|
||||
return Results.BadRequest($"Engineering-Pfad existiert nicht: {req.EngineeringPath}");
|
||||
if (!Directory.Exists(req.SimulationPath))
|
||||
return Results.BadRequest($"Simulations-Pfad existiert nicht: {req.SimulationPath}");
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var project = new ProjectConfig
|
||||
{
|
||||
Name = req.Name,
|
||||
EngineeringPath = req.EngineeringPath,
|
||||
SimulationPath = req.SimulationPath,
|
||||
FileExtensions = req.FileExtensions,
|
||||
IsActive = req.IsActive,
|
||||
BackupEnabled = req.BackupEnabled,
|
||||
BackupPath = req.BackupPath,
|
||||
MaxBackupsPerFile = req.MaxBackupsPerFile
|
||||
};
|
||||
db.Projects.Add(project);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await watcher.StartWatchingAsync(project);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await watcher.ScanExistingFilesAsync(project); }
|
||||
catch (Exception ex) { logger.LogError(ex, "Fehler beim initialen Scan für Projekt {Id}", project.Id); }
|
||||
});
|
||||
|
||||
await hubContext.Clients.All.SendAsync(HubMethodNames.ProjectConfigChanged, CancellationToken.None);
|
||||
|
||||
return Results.Created($"/api/projects/{project.Id}", project);
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async (Guid id, UpdateProjectRequest req,
|
||||
IDbContextFactory<AppDbContext> dbFactory, WatcherService watcher, IHubContext<NotificationHub> hubContext,
|
||||
ILoggerFactory loggerFactory) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger("ProjectsApi");
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var project = await db.Projects.FindAsync(id);
|
||||
if (project is null) return Results.NotFound();
|
||||
|
||||
if (!Directory.Exists(req.EngineeringPath))
|
||||
return Results.BadRequest($"Engineering-Pfad existiert nicht: {req.EngineeringPath}");
|
||||
if (!Directory.Exists(req.SimulationPath))
|
||||
return Results.BadRequest($"Simulations-Pfad existiert nicht: {req.SimulationPath}");
|
||||
|
||||
project.Name = req.Name;
|
||||
project.EngineeringPath = req.EngineeringPath;
|
||||
project.SimulationPath = req.SimulationPath;
|
||||
project.FileExtensions = req.FileExtensions;
|
||||
project.IsActive = req.IsActive;
|
||||
project.BackupEnabled = req.BackupEnabled;
|
||||
project.BackupPath = req.BackupPath;
|
||||
project.MaxBackupsPerFile = req.MaxBackupsPerFile;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Watcher neu starten (stoppt automatisch den alten)
|
||||
await watcher.StartWatchingAsync(project);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await watcher.ScanExistingFilesAsync(project); }
|
||||
catch (Exception ex) { logger.LogError(ex, "Fehler beim initialen Scan für Projekt {Id}", project.Id); }
|
||||
});
|
||||
|
||||
await hubContext.Clients.All.SendAsync(HubMethodNames.ProjectConfigChanged, CancellationToken.None);
|
||||
|
||||
return Results.Ok(project);
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", async (Guid id, IDbContextFactory<AppDbContext> dbFactory,
|
||||
WatcherService watcher, IHubContext<NotificationHub> hubContext) =>
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync();
|
||||
var project = await db.Projects.FindAsync(id);
|
||||
if (project is null) return Results.NotFound();
|
||||
|
||||
await watcher.StopWatchingAsync(id);
|
||||
db.Projects.Remove(project);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await hubContext.Clients.All.SendAsync(HubMethodNames.ProjectConfigChanged, CancellationToken.None);
|
||||
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
group.MapGet("/scan", async (string engineeringPath, string simulationPath, string fileExtensions) =>
|
||||
{
|
||||
if (!Directory.Exists(engineeringPath))
|
||||
return Results.BadRequest($"Engineering-Pfad existiert nicht: {engineeringPath}");
|
||||
if (!Directory.Exists(simulationPath))
|
||||
return Results.BadRequest($"Simulations-Pfad existiert nicht: {simulationPath}");
|
||||
|
||||
var extensions = string.IsNullOrWhiteSpace(fileExtensions) || fileExtensions == "*"
|
||||
? new HashSet<string>()
|
||||
: fileExtensions.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var scanAll = extensions.Count == 0;
|
||||
var engFiles = scanAll
|
||||
? Directory.EnumerateFiles(engineeringPath, "*", SearchOption.AllDirectories).ToList()
|
||||
: Directory.EnumerateFiles(engineeringPath, "*", SearchOption.AllDirectories)
|
||||
.Where(f => extensions.Contains(Path.GetExtension(f)))
|
||||
.ToList();
|
||||
|
||||
var results = new List<ScanResultEntry>();
|
||||
foreach (var engFile in engFiles)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(engineeringPath, engFile);
|
||||
var simFile = Path.Combine(simulationPath, relativePath);
|
||||
|
||||
var engInfo = new FileInfo(engFile);
|
||||
var engHash = await FileHasher.ComputeAsync(engFile);
|
||||
|
||||
var existsInSim = File.Exists(simFile);
|
||||
var needsSync = !existsInSim;
|
||||
if (existsInSim)
|
||||
{
|
||||
var simHash = await FileHasher.ComputeAsync(simFile);
|
||||
needsSync = engHash != simHash;
|
||||
}
|
||||
|
||||
if (needsSync)
|
||||
{
|
||||
results.Add(new ScanResultEntry(
|
||||
relativePath,
|
||||
existsInSim ? "Modified" : "Created",
|
||||
engInfo.Length,
|
||||
engInfo.LastWriteTimeUtc
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(results);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
23
EngineeringSync.Service/EngineeringSync.Service.csproj
Normal file
23
EngineeringSync.Service/EngineeringSync.Service.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-EngineeringSync.Service-6b7e2096-9433-4768-9912-b8f8fa4ad393</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\EngineeringSync.Domain\EngineeringSync.Domain.csproj" />
|
||||
<ProjectReference Include="..\EngineeringSync.Infrastructure\EngineeringSync.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
9
EngineeringSync.Service/Hubs/NotificationHub.cs
Normal file
9
EngineeringSync.Service/Hubs/NotificationHub.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace EngineeringSync.Service.Hubs;
|
||||
|
||||
public class NotificationHub : Hub
|
||||
{
|
||||
// Clients rufen hier keine Server-Methoden auf.
|
||||
// Der Server pusht via IHubContext<NotificationHub>.
|
||||
}
|
||||
39
EngineeringSync.Service/Models/ApiModels.cs
Normal file
39
EngineeringSync.Service/Models/ApiModels.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace EngineeringSync.Service.Models;
|
||||
|
||||
public record CreateProjectRequest(
|
||||
string Name,
|
||||
string EngineeringPath,
|
||||
string SimulationPath,
|
||||
string FileExtensions,
|
||||
bool IsActive = true,
|
||||
bool BackupEnabled = true,
|
||||
string? BackupPath = null,
|
||||
int MaxBackupsPerFile = 0
|
||||
);
|
||||
|
||||
public record UpdateProjectRequest(
|
||||
string Name,
|
||||
string EngineeringPath,
|
||||
string SimulationPath,
|
||||
string FileExtensions,
|
||||
bool IsActive,
|
||||
bool BackupEnabled = true,
|
||||
string? BackupPath = null,
|
||||
int MaxBackupsPerFile = 0
|
||||
);
|
||||
|
||||
public record SyncRequest(List<Guid> ChangeIds);
|
||||
public record IgnoreRequest(List<Guid> ChangeIds);
|
||||
|
||||
public record ScanRequest(
|
||||
string EngineeringPath,
|
||||
string SimulationPath,
|
||||
string FileExtensions
|
||||
);
|
||||
|
||||
public record ScanResultEntry(
|
||||
string RelativePath,
|
||||
string ChangeType,
|
||||
long Size,
|
||||
DateTime LastModified
|
||||
);
|
||||
16
EngineeringSync.Service/Models/FileEvent.cs
Normal file
16
EngineeringSync.Service/Models/FileEvent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace EngineeringSync.Service.Models;
|
||||
|
||||
public record FileEvent(
|
||||
Guid ProjectId,
|
||||
string FullPath,
|
||||
string RelativePath,
|
||||
FileEventType EventType,
|
||||
string? OldRelativePath = null
|
||||
);
|
||||
|
||||
public enum FileEventType
|
||||
{
|
||||
CreatedOrChanged,
|
||||
Renamed,
|
||||
Deleted
|
||||
}
|
||||
151
EngineeringSync.Service/Program.cs
Normal file
151
EngineeringSync.Service/Program.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Text.Json;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using EngineeringSync.Service.Api;
|
||||
using EngineeringSync.Service.Hubs;
|
||||
using EngineeringSync.Service.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseWindowsService();
|
||||
|
||||
// Datenbankpfad: neben der .exe oder im AppData-Ordner (dev: aktuelles Verzeichnis)
|
||||
var dbPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"EngineeringSync", "engineeringsync.db");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
||||
|
||||
builder.Services.AddInfrastructure(dbPath);
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddSingleton<WatcherService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<WatcherService>());
|
||||
builder.Services.AddSingleton<SyncManager>();
|
||||
|
||||
// Kestrel nur auf localhost binden (kein öffentlicher Port)
|
||||
builder.WebHost.UseUrls("http://localhost:5050");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Datenbankmigrationen beim Start
|
||||
await using (var scope = app.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
// WAL-Modus aktivieren (muss nach dem Öffnen der DB gesetzt werden)
|
||||
await db.Database.ExecuteSqlRawAsync("PRAGMA journal_mode=WAL;");
|
||||
|
||||
// First-Run-Config laden (falls vorhanden)
|
||||
var watcher = scope.ServiceProvider.GetRequiredService<WatcherService>();
|
||||
await ProcessFirstRunConfigAsync(db, watcher);
|
||||
}
|
||||
|
||||
async Task ProcessFirstRunConfigAsync(AppDbContext db, WatcherService watcher)
|
||||
{
|
||||
var configPath = Path.Combine(AppContext.BaseDirectory, "firstrun-config.json");
|
||||
if (!File.Exists(configPath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(configPath);
|
||||
var root = JsonDocument.Parse(json);
|
||||
var firstRunElement = root.RootElement.GetProperty("FirstRun");
|
||||
|
||||
var projectName = firstRunElement.GetProperty("ProjectName").GetString() ?? "Imported Project";
|
||||
var engineeringPath = firstRunElement.GetProperty("EngineeringPath").GetString() ?? "";
|
||||
var simulationPath = firstRunElement.GetProperty("SimulationPath").GetString() ?? "";
|
||||
var fileExtensionsRaw = firstRunElement.GetProperty("FileExtensions").GetString() ?? "";
|
||||
var watchAllFiles = firstRunElement.TryGetProperty("WatchAllFiles", out var watchAllElement)
|
||||
? watchAllElement.GetBoolean()
|
||||
: false;
|
||||
|
||||
// Wenn WatchAllFiles=true oder FileExtensions="*", dann leerer String (alle Dateien)
|
||||
var fileExtensions = (watchAllFiles || fileExtensionsRaw == "*") ? "" : fileExtensionsRaw;
|
||||
|
||||
// Backup-Einstellungen (optional, mit Standardwerten für Rückwärtskompatibilität)
|
||||
var backupEnabled = true;
|
||||
string? backupPath = null;
|
||||
var maxBackupsPerFile = 0;
|
||||
|
||||
if (root.RootElement.TryGetProperty("Backup", out var backupElement))
|
||||
{
|
||||
if (backupElement.TryGetProperty("BackupEnabled", out var be))
|
||||
backupEnabled = be.GetBoolean();
|
||||
if (backupElement.TryGetProperty("BackupPath", out var bp) &&
|
||||
bp.ValueKind != JsonValueKind.Null)
|
||||
backupPath = bp.GetString();
|
||||
if (backupElement.TryGetProperty("MaxBackupsPerFile", out var mb))
|
||||
maxBackupsPerFile = mb.GetInt32();
|
||||
}
|
||||
|
||||
// Nur erstellen, wenn die Verzeichnisse existieren
|
||||
if (!Directory.Exists(engineeringPath) || !Directory.Exists(simulationPath))
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[FirstRunConfig] WARNUNG: Engineering- oder Simulations-Pfad existiert nicht. " +
|
||||
$"Engineering={engineeringPath}, Simulation={simulationPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen: Existiert bereits ein Projekt mit diesem Namen?
|
||||
var existingProject = await db.Projects
|
||||
.FirstOrDefaultAsync(p => p.Name == projectName);
|
||||
|
||||
if (existingProject != null)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[FirstRunConfig] Projekt '{projectName}' existiert bereits. Überspringe Import.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Neue ProjectConfig erstellen
|
||||
var project = new EngineeringSync.Domain.Entities.ProjectConfig
|
||||
{
|
||||
Name = projectName,
|
||||
EngineeringPath = engineeringPath,
|
||||
SimulationPath = simulationPath,
|
||||
FileExtensions = fileExtensions,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
BackupEnabled = backupEnabled,
|
||||
BackupPath = backupPath,
|
||||
MaxBackupsPerFile = maxBackupsPerFile
|
||||
};
|
||||
|
||||
db.Projects.Add(project);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
System.Console.WriteLine(
|
||||
$"[FirstRunConfig] Projekt '{projectName}' wurde importiert und aktiviert. " +
|
||||
$"FileExtensions='{fileExtensions}'");
|
||||
|
||||
// Initialer Scan: Unterschiede zwischen Engineering- und Simulations-Ordner erkennen
|
||||
try { await watcher.ScanExistingFilesAsync(project); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[FirstRunConfig] FEHLER beim initialen Scan für '{projectName}': {ex.Message}");
|
||||
}
|
||||
|
||||
// Konfigurationsdatei umbenennen, damit sie beim nächsten Start nicht wieder verarbeitet wird
|
||||
// (WatcherService.ExecuteAsync lädt das Projekt automatisch aus der DB beim Host-Start)
|
||||
var processedPath = configPath + ".processed";
|
||||
if (File.Exists(processedPath))
|
||||
File.Delete(processedPath);
|
||||
File.Move(configPath, processedPath);
|
||||
|
||||
System.Console.WriteLine($"[FirstRunConfig] Konfigurationsdatei verarbeitet: {configPath} → {processedPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.WriteLine(
|
||||
$"[FirstRunConfig] FEHLER beim Verarbeiten der Erstkonfiguration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
app.MapHub<NotificationHub>("/notifications");
|
||||
app.MapProjectsApi();
|
||||
app.MapChangesApi();
|
||||
|
||||
app.Run();
|
||||
12
EngineeringSync.Service/Properties/launchSettings.json
Normal file
12
EngineeringSync.Service/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"EngineeringSync.Service": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
EngineeringSync.Service/Services/FileHasher.cs
Normal file
14
EngineeringSync.Service/Services/FileHasher.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace EngineeringSync.Service.Services;
|
||||
|
||||
public static class FileHasher
|
||||
{
|
||||
public static async Task<string> ComputeAsync(string filePath, CancellationToken ct = default)
|
||||
{
|
||||
await using var stream = new FileStream(filePath,
|
||||
FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true);
|
||||
var hash = await SHA256.HashDataAsync(stream, ct);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
}
|
||||
143
EngineeringSync.Service/Services/SyncManager.cs
Normal file
143
EngineeringSync.Service/Services/SyncManager.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EngineeringSync.Service.Services;
|
||||
|
||||
public class SyncManager(IDbContextFactory<AppDbContext> dbFactory, ILogger<SyncManager> logger)
|
||||
{
|
||||
public async Task<SyncResult> SyncAsync(IEnumerable<Guid> changeIds, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var ids = changeIds.ToList();
|
||||
var changes = await db.PendingChanges
|
||||
.Include(c => c.Project)
|
||||
.Where(c => ids.Contains(c.Id) && c.Status == ChangeStatus.Pending)
|
||||
.ToListAsync(ct);
|
||||
|
||||
int success = 0, failed = 0;
|
||||
foreach (var change in changes)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessChangeAsync(change, ct);
|
||||
change.Status = ChangeStatus.Synced;
|
||||
change.SyncedAt = DateTime.UtcNow;
|
||||
success++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler beim Sync von {Path}", change.RelativePath);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
return new SyncResult(success, failed);
|
||||
}
|
||||
|
||||
public async Task IgnoreAsync(IEnumerable<Guid> changeIds, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var ids = changeIds.ToList();
|
||||
await db.PendingChanges
|
||||
.Where(c => ids.Contains(c.Id) && c.Status == ChangeStatus.Pending)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(c => c.Status, ChangeStatus.Ignored), ct);
|
||||
}
|
||||
|
||||
private async Task ProcessChangeAsync(PendingChange change, CancellationToken ct)
|
||||
{
|
||||
var project = change.Project;
|
||||
var sourcePath = SafeCombine(project.EngineeringPath, change.RelativePath);
|
||||
var targetPath = SafeCombine(project.SimulationPath, change.RelativePath);
|
||||
|
||||
if (change.ChangeType == ChangeType.Deleted)
|
||||
{
|
||||
if (File.Exists(targetPath))
|
||||
{
|
||||
BackupFile(targetPath, project);
|
||||
File.Delete(targetPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (change.ChangeType == ChangeType.Renamed)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(change.OldRelativePath))
|
||||
{
|
||||
var oldTargetPath = SafeCombine(project.SimulationPath, change.OldRelativePath);
|
||||
if (File.Exists(oldTargetPath))
|
||||
File.Delete(oldTargetPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(sourcePath))
|
||||
throw new FileNotFoundException($"Quelldatei nicht gefunden: {sourcePath}");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
||||
|
||||
if (File.Exists(targetPath))
|
||||
BackupFile(targetPath, project);
|
||||
|
||||
await Task.Run(() => File.Copy(sourcePath, targetPath, overwrite: true), ct);
|
||||
}
|
||||
|
||||
private void BackupFile(string targetPath, ProjectConfig project)
|
||||
{
|
||||
if (!project.BackupEnabled)
|
||||
return;
|
||||
|
||||
var backupDir = project.BackupPath ?? Path.GetDirectoryName(targetPath)!;
|
||||
Directory.CreateDirectory(backupDir);
|
||||
|
||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(targetPath);
|
||||
var ext = Path.GetExtension(targetPath);
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var backupPath = Path.Combine(backupDir, $"{nameWithoutExt}_{timestamp}{ext}.bak");
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(targetPath, backupPath);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
File.Copy(targetPath, backupPath, overwrite: false);
|
||||
File.Delete(targetPath);
|
||||
}
|
||||
|
||||
if (project.MaxBackupsPerFile > 0)
|
||||
CleanupOldBackups(backupDir, nameWithoutExt, ext, project.MaxBackupsPerFile);
|
||||
}
|
||||
|
||||
private void CleanupOldBackups(string backupDir, string baseName, string ext, int max)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pattern = $"{baseName}_*{ext}.bak";
|
||||
var backups = Directory.GetFiles(backupDir, pattern)
|
||||
.OrderBy(f => Path.GetFileName(f))
|
||||
.ToList();
|
||||
|
||||
while (backups.Count > max)
|
||||
{
|
||||
File.Delete(backups[0]);
|
||||
backups.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Cleanup alter Backups fehlgeschlagen in {Dir}", backupDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static string SafeCombine(string basePath, string relativePath)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));
|
||||
var normalizedBase = Path.GetFullPath(basePath);
|
||||
if (!fullPath.StartsWith(normalizedBase + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
|
||||
!fullPath.Equals(normalizedBase, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException($"Path traversal detected: {relativePath}");
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
public record SyncResult(int Success, int Failed);
|
||||
434
EngineeringSync.Service/Services/WatcherService.cs
Normal file
434
EngineeringSync.Service/Services/WatcherService.cs
Normal file
@@ -0,0 +1,434 @@
|
||||
using System.Threading.Channels;
|
||||
using EngineeringSync.Domain.Constants;
|
||||
using EngineeringSync.Domain.Entities;
|
||||
using EngineeringSync.Infrastructure;
|
||||
using EngineeringSync.Service.Hubs;
|
||||
using EngineeringSync.Service.Models;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EngineeringSync.Service.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Verwaltet FileSystemWatcher-Instanzen für alle aktiven Projekte.
|
||||
/// Nutzt ein Channel als Puffer zwischen dem schnellen FSW-Event-Thread
|
||||
/// und dem langsamen DB-Schreib-Thread (Debouncing).
|
||||
/// </summary>
|
||||
public sealed class WatcherService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IHubContext<NotificationHub> hub,
|
||||
ILogger<WatcherService> logger) : BackgroundService
|
||||
{
|
||||
// Unbounded Channel: FSW-Events kommen schnell, Verarbeitung ist langsam
|
||||
private readonly Channel<FileEvent> _channel =
|
||||
Channel.CreateUnbounded<FileEvent>(new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
// Watcher pro Projekt-ID – wird dynamisch verwaltet
|
||||
private readonly Dictionary<Guid, FileSystemWatcher[]> _watchers = [];
|
||||
private readonly SemaphoreSlim _watcherLock = new(1, 1);
|
||||
private readonly ILogger<WatcherService> _logger = logger;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Alle aktiven Projekte beim Start laden
|
||||
List<ProjectConfig> projects;
|
||||
await using (var db = await dbFactory.CreateDbContextAsync(stoppingToken))
|
||||
{
|
||||
projects = await db.Projects.Where(p => p.IsActive).ToListAsync(stoppingToken);
|
||||
foreach (var project in projects)
|
||||
await StartWatchingAsync(project);
|
||||
}
|
||||
|
||||
// Initialer Scan: Engineering- vs. Simulations-Ordner vergleichen
|
||||
foreach (var project in projects)
|
||||
{
|
||||
try { await ScanExistingFilesAsync(project, stoppingToken); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Initialer Scan fehlgeschlagen für '{Name}'", project.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Channel-Consumer läuft, bis der Service gestoppt wird
|
||||
await ConsumeChannelAsync(stoppingToken);
|
||||
}
|
||||
|
||||
/// <summary>Startet Watcher für ein Projekt. Idempotent – stoppt alten Watcher zuerst.</summary>
|
||||
public async Task StartWatchingAsync(ProjectConfig project)
|
||||
{
|
||||
await _watcherLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
StopWatchingInternal(project.Id);
|
||||
if (!project.IsActive || !Directory.Exists(project.EngineeringPath))
|
||||
return;
|
||||
|
||||
var extensions = project.GetExtensions().ToArray();
|
||||
|
||||
// Pro Dateiendung einen eigenen Watcher (FSW unterstützt nur ein Filter-Pattern)
|
||||
var watchers = extensions.Select(ext => CreateWatcher(project, ext)).ToArray();
|
||||
_watchers[project.Id] = watchers;
|
||||
_logger.LogInformation("Watcher gestartet für Projekt '{Name}' ({Count} Extensions)",
|
||||
project.Name, watchers.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_watcherLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialer Scan: Vergleicht Engineering- und Simulations-Ordner.
|
||||
/// Erkennt Dateien, die im Engineering-Ordner vorhanden sind, aber im
|
||||
/// Simulations-Ordner fehlen oder sich unterscheiden.
|
||||
/// </summary>
|
||||
public async Task ScanExistingFilesAsync(ProjectConfig project, CancellationToken ct = default)
|
||||
{
|
||||
if (!Directory.Exists(project.EngineeringPath)) return;
|
||||
|
||||
var extensions = project.GetExtensions().ToList();
|
||||
|
||||
// Wenn keine Erweiterungen angegeben oder "*" → alle Dateien scannen
|
||||
var scanAllFiles = extensions.Count == 0 || (extensions.Count == 1 && extensions[0] == "*");
|
||||
|
||||
var engFiles = scanAllFiles
|
||||
? Directory.EnumerateFiles(project.EngineeringPath, "*", SearchOption.AllDirectories).ToList()
|
||||
: Directory.EnumerateFiles(project.EngineeringPath, "*", SearchOption.AllDirectories)
|
||||
.Where(f => extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (engFiles.Count == 0) return;
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var newChanges = 0;
|
||||
|
||||
foreach (var engFile in engFiles)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(project.EngineeringPath, engFile);
|
||||
var simFile = Path.Combine(project.SimulationPath, relativePath);
|
||||
|
||||
var engHash = await FileHasher.ComputeAsync(engFile, ct);
|
||||
var engInfo = new FileInfo(engFile);
|
||||
|
||||
// FileRevision anlegen/aktualisieren
|
||||
var revision = await db.FileRevisions
|
||||
.FirstOrDefaultAsync(r => r.ProjectId == project.Id && r.RelativePath == relativePath, ct);
|
||||
|
||||
if (revision is null)
|
||||
{
|
||||
db.FileRevisions.Add(new FileRevision
|
||||
{
|
||||
ProjectId = project.Id,
|
||||
RelativePath = relativePath,
|
||||
FileHash = engHash,
|
||||
Size = engInfo.Length,
|
||||
LastModified = engInfo.LastWriteTimeUtc
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
revision.FileHash = engHash;
|
||||
revision.Size = engInfo.Length;
|
||||
revision.LastModified = engInfo.LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
// Prüfen: Existiert die Datei im Simulations-Ordner und ist sie identisch?
|
||||
bool needsSync;
|
||||
if (!File.Exists(simFile))
|
||||
{
|
||||
needsSync = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var simHash = await FileHasher.ComputeAsync(simFile, ct);
|
||||
needsSync = engHash != simHash;
|
||||
}
|
||||
|
||||
if (!needsSync) continue;
|
||||
|
||||
// Nur anlegen, wenn nicht bereits ein offener PendingChange existiert
|
||||
var alreadyPending = await db.PendingChanges
|
||||
.AnyAsync(c => c.ProjectId == project.Id
|
||||
&& c.RelativePath == relativePath
|
||||
&& c.Status == ChangeStatus.Pending, ct);
|
||||
if (alreadyPending) continue;
|
||||
|
||||
db.PendingChanges.Add(new PendingChange
|
||||
{
|
||||
ProjectId = project.Id,
|
||||
RelativePath = relativePath,
|
||||
ChangeType = File.Exists(simFile) ? ChangeType.Modified : ChangeType.Created
|
||||
});
|
||||
newChanges++;
|
||||
}
|
||||
|
||||
if (newChanges > 0)
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var totalPending = await db.PendingChanges
|
||||
.CountAsync(c => c.ProjectId == project.Id && c.Status == ChangeStatus.Pending, ct);
|
||||
|
||||
await hub.Clients.All.SendAsync(HubMethodNames.ReceiveChangeNotification,
|
||||
project.Id, project.Name, totalPending, ct);
|
||||
|
||||
_logger.LogInformation("Initialer Scan für '{Name}': {Count} Unterschied(e) erkannt",
|
||||
project.Name, newChanges);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Initialer Scan für '{Name}': Ordner sind synchron", project.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stoppt und entfernt Watcher für ein Projekt.</summary>
|
||||
public async Task StopWatchingAsync(Guid projectId)
|
||||
{
|
||||
await _watcherLock.WaitAsync();
|
||||
try { StopWatchingInternal(projectId); }
|
||||
finally { _watcherLock.Release(); }
|
||||
}
|
||||
|
||||
private void StopWatchingInternal(Guid projectId)
|
||||
{
|
||||
if (!_watchers.Remove(projectId, out var old)) return;
|
||||
foreach (var w in old) { w.EnableRaisingEvents = false; w.Dispose(); }
|
||||
}
|
||||
|
||||
private FileSystemWatcher CreateWatcher(ProjectConfig project, string extension)
|
||||
{
|
||||
var watcher = new FileSystemWatcher(project.EngineeringPath)
|
||||
{
|
||||
Filter = extension == "*" ? "*.*" : $"*{extension}",
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size,
|
||||
InternalBufferSize = 65536 // 64KB – verhindert Buffer-Overflow bei vielen gleichzeitigen Events
|
||||
};
|
||||
|
||||
// Lokale Kopie für Lambda-Capture
|
||||
var projectId = project.Id;
|
||||
var basePath = project.EngineeringPath;
|
||||
|
||||
watcher.Created += (_, e) => EnqueueEvent(projectId, basePath, e.FullPath, FileEventType.CreatedOrChanged);
|
||||
watcher.Changed += (_, e) => EnqueueEvent(projectId, basePath, e.FullPath, FileEventType.CreatedOrChanged);
|
||||
watcher.Deleted += (_, e) => EnqueueEvent(projectId, basePath, e.FullPath, FileEventType.Deleted);
|
||||
watcher.Renamed += (_, e) => EnqueueRenamedEvent(projectId, basePath, e);
|
||||
watcher.Error += (_, args) =>
|
||||
{
|
||||
_logger.LogWarning(args.GetException(), "FSW Buffer-Overflow für Projekt {Id} – starte Re-Scan", projectId);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await ScanExistingFilesAsync(project);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Re-Scan nach Buffer-Overflow fehlgeschlagen für Projekt {Id}", projectId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
return watcher;
|
||||
}
|
||||
|
||||
private void EnqueueEvent(Guid projectId, string basePath, string fullPath, FileEventType type)
|
||||
{
|
||||
var rel = Path.GetRelativePath(basePath, fullPath);
|
||||
_channel.Writer.TryWrite(new FileEvent(projectId, fullPath, rel, type));
|
||||
}
|
||||
|
||||
private void EnqueueRenamedEvent(Guid projectId, string basePath, RenamedEventArgs e)
|
||||
{
|
||||
var rel = Path.GetRelativePath(basePath, e.FullPath);
|
||||
var oldRel = Path.GetRelativePath(basePath, e.OldFullPath);
|
||||
_channel.Writer.TryWrite(new FileEvent(projectId, e.FullPath, rel, FileEventType.Renamed, oldRel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debouncing-Consumer: Gruppiert Events nach (ProjectId, RelativePath) innerhalb
|
||||
/// eines 2000ms-Fensters. Verhindert, dass eine Datei 10× verarbeitet wird.
|
||||
/// </summary>
|
||||
private async Task ConsumeChannelAsync(CancellationToken ct)
|
||||
{
|
||||
// Dictionary: Key=(ProjectId,RelativePath) → letztes Event
|
||||
var pending = new Dictionary<(Guid, string), FileEvent>();
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
// Warte auf erstes Event (blockierend)
|
||||
if (!await _channel.Reader.WaitToReadAsync(ct)) break;
|
||||
|
||||
// Sammle alle sofort verfügbaren Events (nicht blockierend)
|
||||
while (_channel.Reader.TryRead(out var evt))
|
||||
pending[(evt.ProjectId, evt.RelativePath)] = evt; // neuester gewinnt
|
||||
|
||||
// 2s warten – kommen in dieser Zeit weitere Events, werden sie im nächsten Batch verarbeitet
|
||||
await Task.Delay(2000, ct);
|
||||
|
||||
// Noch mehr eingelaufene Events sammeln
|
||||
while (_channel.Reader.TryRead(out var evt))
|
||||
pending[(evt.ProjectId, evt.RelativePath)] = evt;
|
||||
|
||||
if (pending.Count == 0) continue;
|
||||
|
||||
var batch = pending.Values.ToList();
|
||||
pending.Clear();
|
||||
|
||||
foreach (var evt in batch)
|
||||
{
|
||||
try { await ProcessEventAsync(evt, ct); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Fehler beim Verarbeiten von {Path}", evt.RelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessEventAsync(FileEvent evt, CancellationToken ct)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
|
||||
var project = await db.Projects.FindAsync([evt.ProjectId], ct);
|
||||
if (project is null) return;
|
||||
|
||||
// Existierende Revision lesen
|
||||
var revision = await db.FileRevisions
|
||||
.FirstOrDefaultAsync(r => r.ProjectId == evt.ProjectId && r.RelativePath == evt.RelativePath, ct);
|
||||
|
||||
if (evt.EventType == FileEventType.Deleted)
|
||||
{
|
||||
await HandleDeleteAsync(db, project, revision, evt, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(evt.FullPath)) return; // Race condition: Datei schon weg
|
||||
|
||||
// Hash berechnen mit Retry-Logik für gesperrte Dateien
|
||||
string newHash;
|
||||
try
|
||||
{
|
||||
newHash = await ComputeHashWithRetryAsync(evt.FullPath, ct);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Hash-Berechnung für {Path} fehlgeschlagen nach Retrys – überspringe Event", evt.RelativePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (revision is not null && revision.FileHash == newHash) return; // Keine echte Änderung
|
||||
|
||||
var info = new FileInfo(evt.FullPath);
|
||||
|
||||
// FileRevision anlegen oder aktualisieren
|
||||
if (revision is null)
|
||||
{
|
||||
db.FileRevisions.Add(new FileRevision
|
||||
{
|
||||
ProjectId = evt.ProjectId,
|
||||
RelativePath = evt.RelativePath,
|
||||
FileHash = newHash,
|
||||
Size = info.Length,
|
||||
LastModified = info.LastWriteTimeUtc
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
revision.FileHash = newHash;
|
||||
revision.Size = info.Length;
|
||||
revision.LastModified = info.LastWriteTimeUtc;
|
||||
if (evt.EventType == FileEventType.Renamed)
|
||||
revision.RelativePath = evt.RelativePath;
|
||||
}
|
||||
|
||||
// PendingChange schreiben
|
||||
var changeType = evt.EventType switch
|
||||
{
|
||||
FileEventType.Renamed => ChangeType.Renamed,
|
||||
_ when revision is null => ChangeType.Created,
|
||||
_ => ChangeType.Modified
|
||||
};
|
||||
|
||||
db.PendingChanges.Add(new PendingChange
|
||||
{
|
||||
ProjectId = evt.ProjectId,
|
||||
RelativePath = evt.RelativePath,
|
||||
ChangeType = changeType,
|
||||
OldRelativePath = evt.OldRelativePath
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// Alle SignalR-Clients benachrichtigen
|
||||
var count = await db.PendingChanges
|
||||
.CountAsync(c => c.ProjectId == evt.ProjectId && c.Status == ChangeStatus.Pending, ct);
|
||||
|
||||
await hub.Clients.All.SendAsync(HubMethodNames.ReceiveChangeNotification,
|
||||
evt.ProjectId, project.Name, count, ct);
|
||||
|
||||
_logger.LogInformation("[{Type}] {Path} in Projekt '{Name}'",
|
||||
changeType, evt.RelativePath, project.Name);
|
||||
}
|
||||
|
||||
private async Task HandleDeleteAsync(AppDbContext db, ProjectConfig project,
|
||||
FileRevision? revision, FileEvent evt, CancellationToken ct)
|
||||
{
|
||||
if (revision is not null)
|
||||
db.FileRevisions.Remove(revision);
|
||||
|
||||
db.PendingChanges.Add(new PendingChange
|
||||
{
|
||||
ProjectId = evt.ProjectId,
|
||||
RelativePath = evt.RelativePath,
|
||||
ChangeType = ChangeType.Deleted
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var count = await db.PendingChanges
|
||||
.CountAsync(c => c.ProjectId == evt.ProjectId && c.Status == ChangeStatus.Pending, ct);
|
||||
|
||||
await hub.Clients.All.SendAsync(HubMethodNames.ReceiveChangeNotification,
|
||||
evt.ProjectId, project.Name, count, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Berechnet den Hash einer Datei mit Retry-Logik.
|
||||
/// Versucht bis zu 3 Mal, mit exponentieller Backoff-Wartezeit.
|
||||
/// Wirft IOException, wenn alle Versuche scheitern.
|
||||
/// </summary>
|
||||
private async Task<string> ComputeHashWithRetryAsync(string fullPath, CancellationToken ct)
|
||||
{
|
||||
const int maxAttempts = 3;
|
||||
for (int attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await FileHasher.ComputeAsync(fullPath, ct);
|
||||
}
|
||||
catch (IOException) when (attempt < maxAttempts - 1)
|
||||
{
|
||||
var delaySeconds = 2 * (attempt + 1);
|
||||
_logger.LogDebug("Hash-Berechnung für {Path} fehlgeschlagen (Versuch {Attempt}), warte {Seconds}s...",
|
||||
fullPath, attempt + 1, delaySeconds);
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds), ct);
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn alle Versuche fehlschlagen, IOException werfen
|
||||
throw new IOException($"Hash-Berechnung für {fullPath} fehlgeschlagen nach {maxAttempts} Versuchen");
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
// Channel-Writer schließen, um ConsumeChannelAsync zum Beenden zu bringen
|
||||
_channel.Writer.TryComplete();
|
||||
|
||||
foreach (var watchers in _watchers.Values)
|
||||
foreach (var w in watchers) w.Dispose();
|
||||
_watcherLock.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
16
EngineeringSync.Service/Worker.cs
Normal file
16
EngineeringSync.Service/Worker.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace EngineeringSync.Service;
|
||||
|
||||
public class Worker(ILogger<Worker> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
}
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
EngineeringSync.Service/appsettings.Development.json
Normal file
8
EngineeringSync.Service/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
EngineeringSync.Service/appsettings.json
Normal file
8
EngineeringSync.Service/appsettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
EngineeringSync.Setup/App.xaml
Normal file
17
EngineeringSync.Setup/App.xaml
Normal file
@@ -0,0 +1,17 @@
|
||||
<Application x:Class="EngineeringSync.Setup.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:conv="clr-namespace:EngineeringSync.Setup.Converters">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Themes/WizardStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<conv:BoolToVisibilityConverter x:Key="BoolToVisConverter"/>
|
||||
<conv:BoolToInverseVisibilityConverter x:Key="BoolToInvVisConverter"/>
|
||||
<conv:LastStepLabelConverter x:Key="LastStepLabelConverter"/>
|
||||
<conv:StringToVisibilityConverter x:Key="StringToVisConverter"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
82
EngineeringSync.Setup/App.xaml.cs
Normal file
82
EngineeringSync.Setup/App.xaml.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.ServiceProcess;
|
||||
using System.Windows;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EngineeringSync.Setup;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
if (e.Args.Length > 0 && e.Args[0].Equals("/uninstall", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RunUninstall();
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var window = new Views.WizardWindow();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void RunUninstall()
|
||||
{
|
||||
var serviceName = "EngineeringSync";
|
||||
try
|
||||
{
|
||||
var sc = new ServiceController(serviceName);
|
||||
if (sc.Status != ServiceControllerStatus.Stopped)
|
||||
{
|
||||
sc.Stop();
|
||||
sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "sc",
|
||||
Arguments = "delete EngineeringSync",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
})?.WaitForExit();
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
using var runKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true);
|
||||
runKey?.DeleteValue("EngineeringSync.TrayApp", false);
|
||||
}
|
||||
catch { }
|
||||
|
||||
try
|
||||
{
|
||||
Registry.LocalMachine.DeleteSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\EngineeringSync", false);
|
||||
}
|
||||
catch { }
|
||||
|
||||
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
|
||||
var linkPath = Path.Combine(desktop, "EngineeringSync.lnk");
|
||||
if (File.Exists(linkPath))
|
||||
File.Delete(linkPath);
|
||||
|
||||
var startMenu = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
|
||||
"Programs", "EngineeringSync");
|
||||
if (Directory.Exists(startMenu))
|
||||
Directory.Delete(startMenu, true);
|
||||
|
||||
var msg = "EngineeringSync wurde erfolgreich deinstalliert.";
|
||||
MessageBox.Show(msg, "Deinstallation", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
|
||||
10
EngineeringSync.Setup/AssemblyInfo.cs
Normal file
10
EngineeringSync.Setup/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.Setup/Assets/setup-icon.ico
Normal file
BIN
EngineeringSync.Setup/Assets/setup-icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
40
EngineeringSync.Setup/Converters/WizardConverters.cs
Normal file
40
EngineeringSync.Setup/Converters/WizardConverters.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace EngineeringSync.Setup.Converters;
|
||||
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object v, Type t, object p, CultureInfo c) =>
|
||||
v is true ? Visibility.Visible : Visibility.Collapsed;
|
||||
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class BoolToInverseVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object v, Type t, object p, CultureInfo c) =>
|
||||
v is true ? Visibility.Collapsed : Visibility.Visible;
|
||||
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gibt "Jetzt installieren" wenn true (letzter normaler Schritt), sonst "Weiter".
|
||||
/// </summary>
|
||||
public class LastStepLabelConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object v, Type t, object p, CultureInfo c) =>
|
||||
v is true ? "Jetzt installieren" : "Weiter";
|
||||
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class StringToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object v, Type t, object p, CultureInfo c) =>
|
||||
string.IsNullOrEmpty(v as string) ? Visibility.Collapsed : Visibility.Visible;
|
||||
public object ConvertBack(object v, Type t, object p, CultureInfo c) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
23
EngineeringSync.Setup/EngineeringSync.Setup.csproj
Normal file
23
EngineeringSync.Setup/EngineeringSync.Setup.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<AssemblyName>EngineeringSync.Setup</AssemblyName>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\setup-icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\setup-icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
EngineeringSync.Setup/MainWindow.xaml
Normal file
12
EngineeringSync.Setup/MainWindow.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<Window x:Class="EngineeringSync.Setup.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:EngineeringSync.Setup"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
23
EngineeringSync.Setup/MainWindow.xaml.cs
Normal file
23
EngineeringSync.Setup/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.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
304
EngineeringSync.Setup/Services/InstallerService.cs
Normal file
304
EngineeringSync.Setup/Services/InstallerService.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using EngineeringSync.Setup.ViewModels;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace EngineeringSync.Setup.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Führt alle Installationsschritte aus. Gibt Fortschritt und Log-Meldungen
|
||||
/// über Events zurück, damit die UI in Echtzeit aktualisiert werden kann.
|
||||
/// </summary>
|
||||
public class InstallerService(WizardState state)
|
||||
{
|
||||
public event Action<int, string>? Progress;
|
||||
public event Action<string>? LogMessage;
|
||||
|
||||
private void Report(int percent, string step, string? log = null)
|
||||
{
|
||||
Progress?.Invoke(percent, step);
|
||||
LogMessage?.Invoke(log ?? step);
|
||||
}
|
||||
|
||||
/// <summary>Hauptinstallationsablauf – läuft auf einem Background-Thread.</summary>
|
||||
public async Task InstallAsync()
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
// 1. Installationsverzeichnis anlegen
|
||||
Report(5, "Installationsverzeichnis anlegen...");
|
||||
Directory.CreateDirectory(state.InstallPath);
|
||||
await Delay();
|
||||
|
||||
// 2. Programmdateien kopieren
|
||||
Report(15, "Programmdateien werden kopiert...");
|
||||
await CopyApplicationFilesAsync();
|
||||
|
||||
// 3. Datenbank-Verzeichnis anlegen
|
||||
Report(30, "Datenbankverzeichnis anlegen...");
|
||||
var dbDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"EngineeringSync");
|
||||
Directory.CreateDirectory(dbDir);
|
||||
Report(33, "Datenbankverzeichnis angelegt.", $"Datenbank-Pfad: {dbDir}");
|
||||
await Delay();
|
||||
|
||||
// 4. Erstkonfigurations-Datei schreiben
|
||||
Report(40, "Erstkonfiguration wird gespeichert...");
|
||||
WriteFirstRunConfig(dbDir);
|
||||
await Delay();
|
||||
|
||||
// 5. Bestehenden Dienst stoppen & deinstallieren (Upgrade-Szenario)
|
||||
Report(50, "Vorherige Installation wird geprüft...");
|
||||
await StopAndRemoveExistingServiceAsync();
|
||||
await Delay();
|
||||
|
||||
// 6. Windows Service registrieren
|
||||
Report(60, "Windows-Dienst wird registriert...");
|
||||
var serviceExe = Path.Combine(state.InstallPath, "EngineeringSync.Service.exe");
|
||||
await RegisterWindowsServiceAsync(serviceExe);
|
||||
await Delay();
|
||||
|
||||
// 7. Autostart für TrayApp
|
||||
if (state.AutoStartTrayApp)
|
||||
{
|
||||
Report(70, "TrayApp-Autostart wird konfiguriert...");
|
||||
ConfigureTrayAppAutostart();
|
||||
await Delay();
|
||||
}
|
||||
|
||||
// 8. Verknüpfungen erstellen
|
||||
Report(78, "Verknüpfungen werden erstellt...");
|
||||
if (state.CreateDesktopShortcut) CreateShortcut(ShortcutTarget.Desktop);
|
||||
if (state.CreateStartMenuEntry) CreateShortcut(ShortcutTarget.StartMenu);
|
||||
await Delay();
|
||||
|
||||
// 9. Add/Remove Programs Eintrag
|
||||
Report(85, "Deinstallations-Eintrag wird angelegt...");
|
||||
RegisterUninstallEntry();
|
||||
await Delay();
|
||||
|
||||
// 10. Dienst starten
|
||||
if (state.AutoStartService)
|
||||
{
|
||||
Report(92, "Windows-Dienst wird gestartet...");
|
||||
await StartServiceAsync();
|
||||
}
|
||||
|
||||
Report(100, "Installation abgeschlossen.", "✓ EngineeringSync wurde erfolgreich installiert.");
|
||||
});
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────
|
||||
|
||||
private async Task CopyApplicationFilesAsync()
|
||||
{
|
||||
var sourceDir = Path.GetFullPath(AppContext.BaseDirectory).TrimEnd(Path.DirectorySeparatorChar);
|
||||
var targetDir = Path.GetFullPath(state.InstallPath).TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
// Wenn das Setup bereits aus dem Zielverzeichnis läuft (z.B. nach Inno-Setup-Installation),
|
||||
// wurden die Dateien bereits kopiert – nichts zu tun.
|
||||
if (string.Equals(sourceDir, targetDir, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
LogMessage?.Invoke(" Dateien bereits installiert (Inno-Setup) - Kopieren wird übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
var patterns = new[] { "*.exe", "*.dll", "*.json", "*.pdb" };
|
||||
var allFiles = patterns
|
||||
.SelectMany(p => Directory.GetFiles(sourceDir, p))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < allFiles.Count; i++)
|
||||
{
|
||||
var src = allFiles[i];
|
||||
var dest = Path.Combine(targetDir, Path.GetFileName(src));
|
||||
File.Copy(src, dest, overwrite: true);
|
||||
LogMessage?.Invoke($" Kopiert: {Path.GetFileName(src)}");
|
||||
await Task.Delay(30); // Kurze Pause für UI-Responsivität
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteFirstRunConfig(string dbDir)
|
||||
{
|
||||
var config = new
|
||||
{
|
||||
FirstRun = new
|
||||
{
|
||||
ProjectName = state.ProjectName,
|
||||
EngineeringPath = state.EngineeringPath,
|
||||
SimulationPath = state.SimulationPath,
|
||||
FileExtensions = state.WatchAllFiles ? "*" : state.FileExtensions,
|
||||
WatchAllFiles = state.WatchAllFiles
|
||||
},
|
||||
Backup = new // NEU
|
||||
{
|
||||
BackupEnabled = state.BackupEnabled,
|
||||
BackupPath = state.BackupUseCustomPath ? state.BackupCustomPath : (string?)null,
|
||||
MaxBackupsPerFile = state.MaxBackupsPerFile
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(config,
|
||||
new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
var configPath = Path.Combine(state.InstallPath, "firstrun-config.json");
|
||||
File.WriteAllText(configPath, json);
|
||||
LogMessage?.Invoke($" Konfiguration geschrieben: {configPath}");
|
||||
}
|
||||
|
||||
private async Task StopAndRemoveExistingServiceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stopResult = await RunProcessAsync("sc", "stop EngineeringSync");
|
||||
if (stopResult.exitCode == 0)
|
||||
{
|
||||
LogMessage?.Invoke(" Bestehender Dienst gestoppt.");
|
||||
await Task.Delay(1500); // Warten bis Dienst wirklich gestoppt
|
||||
}
|
||||
|
||||
await RunProcessAsync("sc", "delete EngineeringSync");
|
||||
LogMessage?.Invoke(" Bestehender Dienst entfernt.");
|
||||
}
|
||||
catch { /* Kein vorheriger Dienst – OK */ }
|
||||
}
|
||||
|
||||
private async Task RegisterWindowsServiceAsync(string exePath)
|
||||
{
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
LogMessage?.Invoke($" WARNUNG: Service-EXE nicht gefunden: {exePath}");
|
||||
LogMessage?.Invoke(" (Im Entwicklungsmodus – Service-Registrierung übersprungen)");
|
||||
return;
|
||||
}
|
||||
|
||||
var startType = state.AutoStartService ? "auto" : "demand";
|
||||
var args = $"create EngineeringSync binPath= \"{exePath}\" " +
|
||||
$"start= {startType} " +
|
||||
$"DisplayName= \"EngineeringSync Watcher Service\"";
|
||||
|
||||
var (exitCode, output, error) = await RunProcessAsync("sc", args);
|
||||
|
||||
if (exitCode == 0)
|
||||
LogMessage?.Invoke(" ✓ Windows-Dienst registriert.");
|
||||
else
|
||||
{
|
||||
LogMessage?.Invoke($" FEHLER bei sc create (Code {exitCode}): {error}");
|
||||
throw new InvalidOperationException($"Service-Registrierung fehlgeschlagen: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureTrayAppAutostart()
|
||||
{
|
||||
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
|
||||
if (!File.Exists(trayExe))
|
||||
{
|
||||
LogMessage?.Invoke(" TrayApp nicht gefunden – Autostart übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var key = Registry.CurrentUser.OpenSubKey(
|
||||
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", writable: true)!;
|
||||
key.SetValue("EngineeringSync.TrayApp", $"\"{trayExe}\"");
|
||||
LogMessage?.Invoke(" ✓ TrayApp-Autostart in Registry eingetragen.");
|
||||
}
|
||||
|
||||
private enum ShortcutTarget { Desktop, StartMenu }
|
||||
|
||||
private void CreateShortcut(ShortcutTarget target)
|
||||
{
|
||||
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
|
||||
|
||||
var dir = target == ShortcutTarget.Desktop
|
||||
? Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory)
|
||||
: Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
|
||||
"Programs", "EngineeringSync");
|
||||
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var linkPath = Path.Combine(dir, "EngineeringSync.lnk");
|
||||
CreateWindowsShortcut(linkPath, trayExe, state.InstallPath, "EngineeringSync Tray App");
|
||||
|
||||
LogMessage?.Invoke($" ✓ Verknüpfung erstellt: {linkPath}");
|
||||
}
|
||||
|
||||
private static void CreateWindowsShortcut(string linkPath, string targetPath,
|
||||
string workingDir, string description)
|
||||
{
|
||||
// WScript.Shell COM-Interop für Verknüpfungs-Erstellung
|
||||
var type = Type.GetTypeFromProgID("WScript.Shell")!;
|
||||
dynamic shell = Activator.CreateInstance(type)!;
|
||||
var shortcut = shell.CreateShortcut(linkPath);
|
||||
shortcut.TargetPath = targetPath;
|
||||
shortcut.WorkingDirectory = workingDir;
|
||||
shortcut.Description = description;
|
||||
shortcut.Save();
|
||||
Marshal.FinalReleaseComObject(shortcut);
|
||||
Marshal.FinalReleaseComObject(shell);
|
||||
}
|
||||
|
||||
private void RegisterUninstallEntry()
|
||||
{
|
||||
using var key = Registry.LocalMachine.CreateSubKey(
|
||||
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\EngineeringSync");
|
||||
|
||||
var uninstallExe = Path.Combine(state.InstallPath, "EngineeringSync.Setup.exe");
|
||||
key.SetValue("DisplayName", "EngineeringSync");
|
||||
key.SetValue("DisplayVersion", "1.0.0");
|
||||
key.SetValue("Publisher", "EngineeringSync");
|
||||
key.SetValue("InstallLocation", state.InstallPath);
|
||||
key.SetValue("UninstallString", $"\"{uninstallExe}\" /uninstall");
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("EstimatedSize", 45000, RegistryValueKind.DWord);
|
||||
key.SetValue("DisplayIcon", Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe"));
|
||||
|
||||
LogMessage?.Invoke(" ✓ Deinstallations-Eintrag in Registry angelegt.");
|
||||
}
|
||||
|
||||
private async Task StartServiceAsync()
|
||||
{
|
||||
var (exitCode, _, error) = await RunProcessAsync("sc", "start EngineeringSync");
|
||||
if (exitCode == 0)
|
||||
LogMessage?.Invoke(" ✓ Windows-Dienst gestartet.");
|
||||
else
|
||||
LogMessage?.Invoke($" WARNUNG: Dienst konnte nicht gestartet werden ({error})");
|
||||
}
|
||||
|
||||
public void LaunchTrayApp()
|
||||
{
|
||||
var trayExe = Path.Combine(state.InstallPath, "EngineeringSync.TrayApp.exe");
|
||||
if (File.Exists(trayExe))
|
||||
Process.Start(new ProcessStartInfo(trayExe) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
private static async Task<(int exitCode, string output, string error)> RunProcessAsync(
|
||||
string fileName, string arguments)
|
||||
{
|
||||
using var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
return (process.ExitCode, output.Trim(), error.Trim());
|
||||
}
|
||||
|
||||
private static Task Delay() => Task.Delay(400);
|
||||
}
|
||||
361
EngineeringSync.Setup/Themes/WizardStyles.xaml
Normal file
361
EngineeringSync.Setup/Themes/WizardStyles.xaml
Normal file
@@ -0,0 +1,361 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
FARBEN & BRUSHES
|
||||
═══════════════════════════════════════════ -->
|
||||
|
||||
<Color x:Key="AccentColor">#0078D4</Color>
|
||||
<Color x:Key="AccentHoverColor">#106EBE</Color>
|
||||
<Color x:Key="AccentPressColor">#005A9E</Color>
|
||||
<Color x:Key="SidebarTopColor">#1B1B2F</Color>
|
||||
<Color x:Key="SidebarBottomColor">#16213E</Color>
|
||||
<Color x:Key="SuccessColor">#107C10</Color>
|
||||
<Color x:Key="ErrorColor">#C42B1C</Color>
|
||||
<Color x:Key="WarningColor">#CA5010</Color>
|
||||
<Color x:Key="SurfaceColor">#FAFAFA</Color>
|
||||
<Color x:Key="BorderColor">#E0E0E0</Color>
|
||||
<Color x:Key="TextPrimaryColor">#1A1A1A</Color>
|
||||
<Color x:Key="TextSecondaryColor">#5F5F5F</Color>
|
||||
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}"/>
|
||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="{StaticResource AccentHoverColor}"/>
|
||||
<SolidColorBrush x:Key="AccentPressBrush" Color="{StaticResource AccentPressColor}"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="{StaticResource SuccessColor}"/>
|
||||
<SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource ErrorColor}"/>
|
||||
<SolidColorBrush x:Key="WarningBrush" Color="{StaticResource WarningColor}"/>
|
||||
<SolidColorBrush x:Key="SurfaceBrush" Color="{StaticResource SurfaceColor}"/>
|
||||
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}"/>
|
||||
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimaryColor}"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondaryColor}"/>
|
||||
|
||||
<LinearGradientBrush x:Key="SidebarGradient" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="{StaticResource SidebarTopColor}" Offset="0"/>
|
||||
<GradientStop Color="{StaticResource SidebarBottomColor}" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
TYPOGRAPHY
|
||||
═══════════════════════════════════════════ -->
|
||||
<FontFamily x:Key="UiFont">Segoe UI</FontFamily>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
BUTTON STYLES
|
||||
═══════════════════════════════════════════ -->
|
||||
|
||||
<!-- Primärer Akzent-Button -->
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="24,10"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background"
|
||||
Value="{StaticResource AccentHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Background"
|
||||
Value="{StaticResource AccentPressBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Background" Value="#BDBDBD"/>
|
||||
<Setter Property="Foreground" Value="#888888"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Sekundärer Button (Ghost) -->
|
||||
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="20,10"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#F5F5F5"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="border" Property="Background" Value="#EBEBEB"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Foreground" Value="#BDBDBD"/>
|
||||
<Setter TargetName="border" Property="BorderBrush" Value="#E0E0E0"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Icon-Button für Folder-Browse -->
|
||||
<Style x:Key="IconButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="Width" Value="36"/>
|
||||
<Setter Property="Height" Value="36"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="Background" Value="#F0F7FF"/>
|
||||
<Setter TargetName="bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
TEXTBOX STYLE
|
||||
═══════════════════════════════════════════ -->
|
||||
<Style x:Key="ModernTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="border"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6">
|
||||
<ScrollViewer x:Name="PART_ContentHost" Margin="2,0"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="border" Property="BorderBrush"
|
||||
Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="border" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="border" Property="Background" Value="#F5F5F5"/>
|
||||
<Setter Property="Foreground" Value="#BDBDBD"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
CHECKBOX STYLE
|
||||
═══════════════════════════════════════════ -->
|
||||
<Style x:Key="ModernCheckBoxStyle" TargetType="CheckBox">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border x:Name="box"
|
||||
Width="18" Height="18"
|
||||
CornerRadius="4"
|
||||
BorderThickness="2"
|
||||
BorderBrush="{StaticResource BorderBrush}"
|
||||
Background="White"
|
||||
Margin="0,0,8,0">
|
||||
<TextBlock x:Name="check"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="Collapsed"/>
|
||||
</Border>
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="box" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="check" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
LABELS & TYPOGRAPHY
|
||||
═══════════════════════════════════════════ -->
|
||||
|
||||
<Style x:Key="PageTitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="24"/>
|
||||
<Setter Property="FontWeight" Value="Light"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PageSubtitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Margin" Value="0,0,0,24"/>
|
||||
<Setter Property="LineHeight" Value="20"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
SIDEBAR STEP ITEM
|
||||
═══════════════════════════════════════════ -->
|
||||
<Style x:Key="StepItemStyle" TargetType="ContentControl">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContentControl">
|
||||
<Grid Margin="0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="36"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Kreis-Indikator -->
|
||||
<Border x:Name="circle"
|
||||
Width="28" Height="28"
|
||||
CornerRadius="14"
|
||||
Background="#2A3050"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock x:Name="iconText"
|
||||
Text="{Binding Icon}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12"
|
||||
Foreground="#8899BB"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- Titel -->
|
||||
<TextBlock x:Name="titleText"
|
||||
Grid.Column="1"
|
||||
Text="{Binding Title}"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="12"
|
||||
Foreground="#8899BB"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<!-- Aktiver Schritt -->
|
||||
<DataTrigger Binding="{Binding IsActive}" Value="True">
|
||||
<Setter TargetName="circle" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="iconText" Property="Foreground" Value="White"/>
|
||||
<Setter TargetName="titleText" Property="Foreground" Value="White"/>
|
||||
<Setter TargetName="titleText" Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
<!-- Abgeschlossener Schritt -->
|
||||
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
|
||||
<Setter TargetName="circle" Property="Background" Value="#107C10"/>
|
||||
<Setter TargetName="iconText" Property="Text" Value=""/>
|
||||
<Setter TargetName="iconText" Property="Foreground" Value="White"/>
|
||||
<Setter TargetName="titleText" Property="Foreground" Value="#88CCAA"/>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
INFO CARD (für Zusammenfassung etc.)
|
||||
═══════════════════════════════════════════ -->
|
||||
<Style x:Key="InfoCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
|
||||
<!-- ═══════════════════════════════════════════
|
||||
PROGRESS BAR
|
||||
═══════════════════════════════════════════ -->
|
||||
<Style x:Key="ModernProgressBarStyle" TargetType="ProgressBar">
|
||||
<Setter Property="Height" Value="6"/>
|
||||
<Setter Property="Background" Value="#E0E0E0"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ProgressBar">
|
||||
<Grid>
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="3"/>
|
||||
<Border x:Name="PART_Track" CornerRadius="3" ClipToBounds="True">
|
||||
<Border x:Name="PART_Indicator"
|
||||
Background="{TemplateBinding Foreground}"
|
||||
CornerRadius="3"
|
||||
HorizontalAlignment="Left"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
36
EngineeringSync.Setup/ViewModels/WizardState.cs
Normal file
36
EngineeringSync.Setup/ViewModels/WizardState.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.IO;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace EngineeringSync.Setup.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Zentrales Datenmodell das durch alle Wizard-Schritte weitergereicht wird.
|
||||
/// </summary>
|
||||
public partial class WizardState : ObservableObject
|
||||
{
|
||||
// --- Installationspfad ---
|
||||
[ObservableProperty]
|
||||
private string _installPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||||
"EngineeringSync");
|
||||
|
||||
// --- Erstes Projekt ---
|
||||
[ObservableProperty] private string _projectName = "Mein Projekt";
|
||||
[ObservableProperty] private string _engineeringPath = string.Empty;
|
||||
[ObservableProperty] private string _simulationPath = string.Empty;
|
||||
[ObservableProperty] private bool _watchAllFiles = false;
|
||||
[ObservableProperty] private string _fileExtensions = ".jt,.cojt,.xml";
|
||||
|
||||
// --- Service-Optionen ---
|
||||
[ObservableProperty] private bool _autoStartService = true;
|
||||
[ObservableProperty] private bool _autoStartTrayApp = true;
|
||||
[ObservableProperty] private bool _startAfterInstall = true;
|
||||
[ObservableProperty] private bool _createDesktopShortcut = true;
|
||||
[ObservableProperty] private bool _createStartMenuEntry = true;
|
||||
|
||||
// --- Backup-Einstellungen ---
|
||||
[ObservableProperty] private bool _backupEnabled = true;
|
||||
[ObservableProperty] private bool _backupUseCustomPath = false;
|
||||
[ObservableProperty] private string _backupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _maxBackupsPerFile = 0;
|
||||
}
|
||||
135
EngineeringSync.Setup/ViewModels/WizardViewModel.cs
Normal file
135
EngineeringSync.Setup/ViewModels/WizardViewModel.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using EngineeringSync.Setup.Services;
|
||||
using EngineeringSync.Setup.Views.Pages;
|
||||
|
||||
namespace EngineeringSync.Setup.ViewModels;
|
||||
|
||||
public partial class WizardViewModel : ObservableObject
|
||||
{
|
||||
public WizardState State { get; } = new();
|
||||
|
||||
[ObservableProperty] private WizardPageBase _currentPage = null!;
|
||||
[ObservableProperty] private int _currentStepIndex;
|
||||
[ObservableProperty] private bool _canGoBack;
|
||||
[ObservableProperty] private bool _canGoNext = true;
|
||||
[ObservableProperty] private bool _isLastStep;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowNextButton))]
|
||||
private bool _isInstalling;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowNextButton))]
|
||||
private bool _isCompleted;
|
||||
|
||||
public bool ShowNextButton => !IsCompleted && !IsInstalling;
|
||||
|
||||
public ObservableCollection<WizardStep> Steps { get; } =
|
||||
[
|
||||
new("Willkommen", "\uE80F"), // Home icon
|
||||
new("Installation", "\uE7B7"), // Folder icon
|
||||
new("Erstes Projekt","\uE8B7"), // Link icon
|
||||
new("Backup", "\uE72E"), // Save icon (NEU)
|
||||
new("Optionen", "\uE713"), // Settings icon
|
||||
new("Zusammenfassung","\uE8A9"), // List icon
|
||||
new("Installation", "\uE896"), // Download icon
|
||||
new("Fertig", "\uE930"), // Completed icon
|
||||
];
|
||||
|
||||
private readonly List<Func<WizardPageBase>> _pageFactories;
|
||||
private readonly InstallerService _installer;
|
||||
|
||||
public WizardViewModel()
|
||||
{
|
||||
_installer = new InstallerService(State);
|
||||
|
||||
_pageFactories =
|
||||
[
|
||||
() => new WelcomePage(this),
|
||||
() => new InstallPathPage(this),
|
||||
() => new FirstProjectPage(this),
|
||||
() => new BackupOptionsPage(this), // NEU – Index 3
|
||||
() => new ServiceOptionsPage(this),
|
||||
() => new SummaryPage(this),
|
||||
() => new InstallingPage(this, _installer),
|
||||
() => new CompletionPage(this),
|
||||
];
|
||||
|
||||
NavigateTo(0);
|
||||
}
|
||||
|
||||
public void NavigateTo(int index)
|
||||
{
|
||||
CurrentStepIndex = index;
|
||||
CurrentPage = _pageFactories[index]();
|
||||
Steps[index].IsActive = true;
|
||||
|
||||
for (int i = 0; i < Steps.Count; i++)
|
||||
{
|
||||
Steps[i].IsCompleted = i < index;
|
||||
Steps[i].IsActive = i == index;
|
||||
}
|
||||
|
||||
CanGoBack = index > 0 && index < Steps.Count - 1 && !IsInstalling;
|
||||
IsLastStep = index == Steps.Count - 2; // "Zusammenfassung" ist letzter normaler Schritt
|
||||
// IsInstalling wird NICHT hier gesetzt – wird von InstallingPage kontrolliert
|
||||
IsCompleted = index == Steps.Count - 1;
|
||||
CanGoNext = !IsInstalling && !IsCompleted;
|
||||
}
|
||||
|
||||
public void SetInstallingState(bool installing)
|
||||
{
|
||||
IsInstalling = installing;
|
||||
// CanGoNext wird durch die Bedingung !IsInstalling && !IsCompleted neu evaluiert
|
||||
CanGoNext = !IsInstalling && !IsCompleted;
|
||||
// GoBackCommand und CancelCommand CanExecute Bedingungen neu evaluieren
|
||||
GoBackCommand.NotifyCanExecuteChanged();
|
||||
CancelCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteGoBack))]
|
||||
private void GoBack() => NavigateTo(CurrentStepIndex - 1);
|
||||
|
||||
private bool CanExecuteGoBack => CanGoBack && !IsInstalling;
|
||||
|
||||
[RelayCommand]
|
||||
private void GoNext()
|
||||
{
|
||||
if (!CurrentPage.Validate()) return;
|
||||
if (CurrentStepIndex < _pageFactories.Count - 1)
|
||||
NavigateTo(CurrentStepIndex + 1);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanExecuteCancel))]
|
||||
private void Cancel()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
var result = MessageBox.Show(
|
||||
"Setup wirklich abbrechen?",
|
||||
"EngineeringSync Setup",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
if (result == MessageBoxResult.Yes)
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
|
||||
private bool CanExecuteCancel => !IsInstalling && !IsCompleted;
|
||||
|
||||
[RelayCommand]
|
||||
private void Finish()
|
||||
{
|
||||
if (State.StartAfterInstall)
|
||||
_installer.LaunchTrayApp();
|
||||
Application.Current.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class WizardStep(string title, string icon) : ObservableObject
|
||||
{
|
||||
public string Title { get; } = title;
|
||||
public string Icon { get; } = icon;
|
||||
[ObservableProperty] private bool _isActive;
|
||||
[ObservableProperty] private bool _isCompleted;
|
||||
}
|
||||
86
EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml
Normal file
86
EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml
Normal 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="" 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="" 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>
|
||||
39
EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs
Normal file
39
EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
62
EngineeringSync.Setup/Views/Pages/CompletionPage.xaml
Normal file
62
EngineeringSync.Setup/Views/Pages/CompletionPage.xaml
Normal 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="" 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="" 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="" 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 "Schließen" um den Assistenten zu beenden."/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</local:WizardPageBase>
|
||||
11
EngineeringSync.Setup/Views/Pages/CompletionPage.xaml.cs
Normal file
11
EngineeringSync.Setup/Views/Pages/CompletionPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
17
EngineeringSync.Setup/Views/Pages/FeatureRow.xaml
Normal file
17
EngineeringSync.Setup/Views/Pages/FeatureRow.xaml
Normal 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>
|
||||
25
EngineeringSync.Setup/Views/Pages/FeatureRow.xaml.cs
Normal file
25
EngineeringSync.Setup/Views/Pages/FeatureRow.xaml.cs
Normal 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();
|
||||
}
|
||||
138
EngineeringSync.Setup/Views/Pages/FirstProjectPage.xaml
Normal file
138
EngineeringSync.Setup/Views/Pages/FirstProjectPage.xaml
Normal 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="" 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="" 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>
|
||||
79
EngineeringSync.Setup/Views/Pages/FirstProjectPage.xaml.cs
Normal file
79
EngineeringSync.Setup/Views/Pages/FirstProjectPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
10
EngineeringSync.Setup/Views/Pages/InstallItem.xaml
Normal file
10
EngineeringSync.Setup/Views/Pages/InstallItem.xaml
Normal 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="" 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>
|
||||
14
EngineeringSync.Setup/Views/Pages/InstallItem.xaml.cs
Normal file
14
EngineeringSync.Setup/Views/Pages/InstallItem.xaml.cs
Normal 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();
|
||||
}
|
||||
61
EngineeringSync.Setup/Views/Pages/InstallPathPage.xaml
Normal file
61
EngineeringSync.Setup/Views/Pages/InstallPathPage.xaml
Normal 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="" 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="" 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="" 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>
|
||||
35
EngineeringSync.Setup/Views/Pages/InstallPathPage.xaml.cs
Normal file
35
EngineeringSync.Setup/Views/Pages/InstallPathPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
65
EngineeringSync.Setup/Views/Pages/InstallingPage.xaml
Normal file
65
EngineeringSync.Setup/Views/Pages/InstallingPage.xaml
Normal 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>
|
||||
102
EngineeringSync.Setup/Views/Pages/InstallingPage.xaml.cs
Normal file
102
EngineeringSync.Setup/Views/Pages/InstallingPage.xaml.cs
Normal 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.";
|
||||
});
|
||||
}
|
||||
}
|
||||
32
EngineeringSync.Setup/Views/Pages/OptionCard.xaml
Normal file
32
EngineeringSync.Setup/Views/Pages/OptionCard.xaml
Normal 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>
|
||||
47
EngineeringSync.Setup/Views/Pages/OptionCard.xaml.cs
Normal file
47
EngineeringSync.Setup/Views/Pages/OptionCard.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
51
EngineeringSync.Setup/Views/Pages/ServiceOptionsPage.xaml
Normal file
51
EngineeringSync.Setup/Views/Pages/ServiceOptionsPage.xaml
Normal 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="" Title="Windows-Dienst automatisch starten"
|
||||
Description="Der EngineeringSync-Dienst startet automatisch mit Windows (empfohlen)"
|
||||
IsChecked="{Binding AutoStartService, Mode=TwoWay}"/>
|
||||
|
||||
<local:OptionCard Icon="" 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="" Title="Desktop-Verknüpfung erstellen"
|
||||
Description="Erstellt eine Verknüpfung zur Tray-App auf dem Desktop"
|
||||
IsChecked="{Binding CreateDesktopShortcut, Mode=TwoWay}"/>
|
||||
|
||||
<local:OptionCard Icon="" 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="" 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>
|
||||
11
EngineeringSync.Setup/Views/Pages/ServiceOptionsPage.xaml.cs
Normal file
11
EngineeringSync.Setup/Views/Pages/ServiceOptionsPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
18
EngineeringSync.Setup/Views/Pages/SummaryBoolRow.xaml
Normal file
18
EngineeringSync.Setup/Views/Pages/SummaryBoolRow.xaml
Normal 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>
|
||||
40
EngineeringSync.Setup/Views/Pages/SummaryBoolRow.xaml.cs
Normal file
40
EngineeringSync.Setup/Views/Pages/SummaryBoolRow.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
92
EngineeringSync.Setup/Views/Pages/SummaryPage.xaml
Normal file
92
EngineeringSync.Setup/Views/Pages/SummaryPage.xaml
Normal 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 "Jetzt installieren" 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="" 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="" 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="" 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="" 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>
|
||||
11
EngineeringSync.Setup/Views/Pages/SummaryPage.xaml.cs
Normal file
11
EngineeringSync.Setup/Views/Pages/SummaryPage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
16
EngineeringSync.Setup/Views/Pages/SummaryRow.xaml
Normal file
16
EngineeringSync.Setup/Views/Pages/SummaryRow.xaml
Normal 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>
|
||||
19
EngineeringSync.Setup/Views/Pages/SummaryRow.xaml.cs
Normal file
19
EngineeringSync.Setup/Views/Pages/SummaryRow.xaml.cs
Normal 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();
|
||||
}
|
||||
63
EngineeringSync.Setup/Views/Pages/WelcomePage.xaml
Normal file
63
EngineeringSync.Setup/Views/Pages/WelcomePage.xaml
Normal 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="" 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="" Title="EngineeringSync Service"
|
||||
Description="Windows-Dienst der Ihr Engineering-Verzeichnis überwacht und Änderungen protokolliert"/>
|
||||
<local:FeatureRow Icon="" Title="System Tray App"
|
||||
Description="Benachrichtigt Sie sofort über neue Änderungen und ermöglicht kontrollierten Sync"/>
|
||||
<local:FeatureRow Icon="" 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="" 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>
|
||||
11
EngineeringSync.Setup/Views/Pages/WelcomePage.xaml.cs
Normal file
11
EngineeringSync.Setup/Views/Pages/WelcomePage.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
25
EngineeringSync.Setup/Views/Pages/WizardPageBase.cs
Normal file
25
EngineeringSync.Setup/Views/Pages/WizardPageBase.cs
Normal 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;
|
||||
}
|
||||
251
EngineeringSync.Setup/Views/WizardWindow.xaml
Normal file
251
EngineeringSync.Setup/Views/WizardWindow.xaml
Normal 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=""
|
||||
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=""
|
||||
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=""
|
||||
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=" "
|
||||
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=""
|
||||
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>
|
||||
24
EngineeringSync.Setup/Views/WizardWindow.xaml.cs
Normal file
24
EngineeringSync.Setup/Views/WizardWindow.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
25
EngineeringSync.Setup/app.manifest
Normal file
25
EngineeringSync.Setup/app.manifest
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="EngineeringSync.Setup.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- Adminrechte für Service-Installation erforderlich -->
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 11 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
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;
|
||||
}
|
||||
}
|
||||
7
EngineeringSync.slnx
Normal file
7
EngineeringSync.slnx
Normal file
@@ -0,0 +1,7 @@
|
||||
<Solution>
|
||||
<Project Path="EngineeringSync.Domain/EngineeringSync.Domain.csproj" />
|
||||
<Project Path="EngineeringSync.Infrastructure/EngineeringSync.Infrastructure.csproj" />
|
||||
<Project Path="EngineeringSync.Service/EngineeringSync.Service.csproj" />
|
||||
<Project Path="EngineeringSync.Setup/EngineeringSync.Setup.csproj" />
|
||||
<Project Path="EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj" />
|
||||
</Solution>
|
||||
712
HANDBUCH.md
Normal file
712
HANDBUCH.md
Normal file
@@ -0,0 +1,712 @@
|
||||
# EngineeringSync – Benutzerhandbuch
|
||||
|
||||
**Version 1.0.0** | Stand: März 2026
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Einführung](#1-einführung)
|
||||
2. [Systemvoraussetzungen](#2-systemvoraussetzungen)
|
||||
3. [Installation](#3-installation)
|
||||
- 3.1 [Setup-Assistent starten](#31-setup-assistent-starten)
|
||||
- 3.2 [Schritt: Willkommen](#32-schritt-willkommen)
|
||||
- 3.3 [Schritt: Installation](#33-schritt-installation)
|
||||
- 3.4 [Schritt: Erstes Projekt konfigurieren](#34-schritt-erstes-projekt-konfigurieren)
|
||||
- 3.5 [Schritt: Backup-Einstellungen](#35-schritt-backup-einstellungen)
|
||||
- 3.6 [Schritt: Start- und Verknüpfungs-Optionen](#36-schritt-start--und-verknüpfungs-optionen)
|
||||
- 3.7 [Schritt: Zusammenfassung & Installation](#37-schritt-zusammenfassung--installation)
|
||||
- 3.8 [Schritt: Fertig](#38-schritt-fertig)
|
||||
4. [Die Tray-App im Überblick](#4-die-tray-app-im-überblick)
|
||||
5. [Projekte verwalten](#5-projekte-verwalten)
|
||||
- 5.1 [Neues Projekt anlegen](#51-neues-projekt-anlegen)
|
||||
- 5.2 [Projekt bearbeiten](#52-projekt-bearbeiten)
|
||||
- 5.3 [Projekt löschen](#53-projekt-löschen)
|
||||
- 5.4 [Dateiüberwachung konfigurieren](#54-dateiüberwachung-konfigurieren)
|
||||
6. [Änderungen überwachen und synchronisieren](#6-änderungen-überwachen-und-synchronisieren)
|
||||
- 6.1 [Benachrichtigungen empfangen](#61-benachrichtigungen-empfangen)
|
||||
- 6.2 [Das Fenster „Ausstehende Änderungen"](#62-das-fenster-ausstehende-änderungen)
|
||||
- 6.3 [Änderungen synchronisieren](#63-änderungen-synchronisieren)
|
||||
- 6.4 [Änderungen ignorieren](#64-änderungen-ignorieren)
|
||||
- 6.5 [Änderungshistorie](#65-änderungshistorie)
|
||||
7. [Backup-Funktion](#7-backup-funktion)
|
||||
8. [Windows-Dienst verwalten](#8-windows-dienst-verwalten)
|
||||
9. [Deinstallation](#9-deinstallation)
|
||||
10. [Technische Architektur](#10-technische-architektur)
|
||||
11. [Fehlerbehebung & FAQ](#11-fehlerbehebung--faq)
|
||||
|
||||
---
|
||||
|
||||
## 1. Einführung
|
||||
|
||||
**EngineeringSync** ist ein Middleware-Werkzeug, das die Arbeit zwischen der Konstruktionsabteilung (CAD) und der Simulationsabteilung (z. B. Process Simulate) koordiniert.
|
||||
|
||||
### Das Problem ohne EngineeringSync
|
||||
|
||||
In der täglichen Praxis arbeiten Konstrukteure und Simulationsingenieure an denselben Dateien, aber in unterschiedlichen Verzeichnissen. Dateien werden manuell kopiert – oft zur falschen Zeit, in der falschen Version, oder sogar während eine Simulation läuft. Das kann zu:
|
||||
|
||||
- **Datenverlust** führen (Überschreiben aktiver Simulationsdateien)
|
||||
- **Inkonsistenz** erzeugen (Simulation basiert auf veralteten CAD-Daten)
|
||||
- **Zeitverlust** verursachen (manuelles Tracking von Änderungen)
|
||||
|
||||
### Die Lösung mit EngineeringSync
|
||||
|
||||
```
|
||||
Konstrukteur Simulationsingenieur
|
||||
│ │
|
||||
│ speichert CAD-Datei │
|
||||
▼ │
|
||||
Engineering-Ordner Simulations-Ordner
|
||||
(wird überwacht) (Sync-Ziel)
|
||||
│ │
|
||||
└──────── EngineeringSync ──────────┘
|
||||
│
|
||||
├─ erkennt Änderungen automatisch
|
||||
├─ benachrichtigt den Sim-Ingenieur
|
||||
├─ wartet auf Freigabe
|
||||
└─ kopiert erst nach Bestätigung
|
||||
```
|
||||
|
||||
Der Simulationsingenieur behält die volle Kontrolle: Dateien werden **nie automatisch** überschrieben – erst nach expliziter Freigabe durch die Tray-App.
|
||||
|
||||
---
|
||||
|
||||
## 2. Systemvoraussetzungen
|
||||
|
||||
| Komponente | Mindestanforderung |
|
||||
|---|---|
|
||||
| Betriebssystem | Windows 10 (64-Bit) oder Windows 11 |
|
||||
| .NET Runtime | .NET 10 (wird vom Installer mitgeliefert) |
|
||||
| Arbeitsspeicher | 256 MB RAM (Dienst) |
|
||||
| Festplatte | 150 MB freier Speicher |
|
||||
| Netzwerk | Keine (rein lokal, `localhost:5050`) |
|
||||
| Benutzerrechte | Administratorrechte für die Installation |
|
||||
|
||||
> **Hinweis:** EngineeringSync läuft ausschließlich lokal auf einem Rechner. Netzwerkfreigaben können als überwachte Pfade genutzt werden, sofern sie als Laufwerksbuchstabe eingebunden sind.
|
||||
|
||||
---
|
||||
|
||||
## 3. Installation
|
||||
|
||||
### 3.1 Setup-Assistent starten
|
||||
|
||||
Führen Sie die Installationsdatei `EngineeringSync-Setup.exe` aus. Falls eine Sicherheitswarnung von Windows erscheint, klicken Sie auf **„Weitere Informationen"** → **„Trotzdem ausführen"**.
|
||||
|
||||
Der Setup-Assistent öffnet sich als modernes Fenster mit einer linken Navigationsleiste, die den aktuellen Fortschritt anzeigt.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Schritt: Willkommen
|
||||
|
||||
Der erste Schritt begrüßt Sie und gibt einen Überblick über die installierten Komponenten:
|
||||
|
||||
- **EngineeringSync-Dienst** – der Hintergrunddienst, der Dateien überwacht
|
||||
- **Tray-App** – die Benutzeroberfläche im Windows-Systemtray
|
||||
- **SQLite-Datenbank** – lokale Speicherung aller Änderungen und Konfigurationen
|
||||
|
||||
Klicken Sie auf **„Weiter →"** um fortzufahren.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Schritt: Installation
|
||||
|
||||
Wählen Sie den **Installationsordner**. Der Standardpfad lautet:
|
||||
|
||||
```
|
||||
C:\Program Files\EngineeringSync\
|
||||
```
|
||||
|
||||
Sie können über das Ordner-Symbol einen anderen Pfad wählen. Klicken Sie auf **„Weiter →"**.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Schritt: Erstes Projekt konfigurieren
|
||||
|
||||
Dies ist der wichtigste Konfigurationsschritt. Sie definieren hier Ihr erstes überwachtes Projekt.
|
||||
|
||||
#### Projektname
|
||||
|
||||
Vergeben Sie einen aussagekräftigen Namen, z. B. `Linie 3 – Roboterzelle` oder `Projekt Alpha`.
|
||||
|
||||
#### Engineering-Quellpfad *(wird überwacht)*
|
||||
|
||||
Der Ordner, in dem der **Konstrukteur** arbeitet und CAD-Dateien speichert. Jede Änderung in diesem Verzeichnis wird automatisch erfasst und dem Simulationsingenieur gemeldet.
|
||||
|
||||
Beispiel: `C:\CAD-Projekte\Linie3\`
|
||||
|
||||
> Klicken Sie auf das **Ordner-Symbol** rechts neben dem Eingabefeld um einen Ordner per Dialog auszuwählen.
|
||||
|
||||
#### Simulations-Zielpfad *(Sync-Ziel)*
|
||||
|
||||
Der Ordner, in dem der **Simulationsingenieur** arbeitet. Freigegebene Änderungen werden in dieses Verzeichnis kopiert (mit automatischem Backup der bestehenden Dateien).
|
||||
|
||||
Beispiel: `C:\ProcessSimulate\Projekte\Linie3\`
|
||||
|
||||
#### Überwachte Dateien
|
||||
|
||||
Sie können wählen, welche Dateitypen überwacht werden:
|
||||
|
||||
| Option | Beschreibung |
|
||||
|---|---|
|
||||
| **Alle Dateitypen überwachen** | Jede Dateiänderung im Ordner wird protokolliert (unabhängig von der Endung) |
|
||||
| **Spezifische Endungen** | Nur bestimmte Dateitypen werden überwacht |
|
||||
|
||||
Für spezifische Endungen stehen Quick-Select-Buttons bereit:
|
||||
|
||||
| Button | Format | Verwendung |
|
||||
|---|---|---|
|
||||
| `.jt` | JT (Jupiter Tesselation) | Siemens NX, Teamcenter |
|
||||
| `.cojt` | Compound JT | Process Simulate Assemblies |
|
||||
| `.xml` | XML | Konfigurationsdateien |
|
||||
| `.stp` / `.step` | STEP | CAD-Austauschformat |
|
||||
| `.igs` / `.iges` | IGES | Älteres CAD-Austauschformat |
|
||||
| `.prt` | Part File | NX Part-Dateien |
|
||||
|
||||
Sie können auch manuell Endungen (komma-getrennt) eingeben, z. B.:
|
||||
`.jt, .cojt, .xml, .prt`
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Schritt: Backup-Einstellungen
|
||||
|
||||
Bevor EngineeringSync eine Simulationsdatei überschreibt, kann automatisch eine Sicherungskopie erstellt werden.
|
||||
|
||||
#### Backups aktivieren
|
||||
|
||||
Aktivieren Sie den Schalter um Backups zu aktivieren (empfohlen). Das Backup wird unmittelbar **vor** dem Überschreiben erstellt.
|
||||
|
||||
Das Namensschema der Backup-Datei lautet:
|
||||
`{Dateiname}_{JJJJMMTT_HHMMSS}.bak`
|
||||
|
||||
Beispiel: `Roboter_Arm.jt_20260315_143022.bak`
|
||||
|
||||
#### Backup-Speicherort
|
||||
|
||||
| Option | Beschreibung |
|
||||
|---|---|
|
||||
| **Gleicher Ordner** | Das Backup liegt neben der Originaldatei (Standard) |
|
||||
| **Eigener Backup-Ordner** | Alle Backups werden zentral in einem Ordner gesammelt |
|
||||
|
||||
#### Aufbewahrung
|
||||
|
||||
Legen Sie fest, wie viele Backups **pro Datei** maximal behalten werden:
|
||||
|
||||
- `0` = unbegrenzt (alle Backups behalten)
|
||||
- `5` = die letzten 5 Backups werden behalten, ältere werden gelöscht
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Schritt: Start- und Verknüpfungs-Optionen
|
||||
|
||||
#### Autostart
|
||||
|
||||
| Option | Empfehlung | Beschreibung |
|
||||
|---|---|---|
|
||||
| **Windows-Dienst automatisch starten** | ✅ Empfohlen | Der Hintergrunddienst startet mit Windows |
|
||||
| **Tray-App bei Anmeldung starten** | ✅ Empfohlen | Die Tray-App erscheint nach dem Login automatisch |
|
||||
|
||||
#### Verknüpfungen
|
||||
|
||||
| Option | Beschreibung |
|
||||
|---|---|
|
||||
| **Desktop-Verknüpfung** | Startet die Tray-App per Doppelklick vom Desktop |
|
||||
| **Startmenü-Eintrag** | EngineeringSync im Windows-Startmenü |
|
||||
|
||||
#### Nach Installation
|
||||
|
||||
| Option | Beschreibung |
|
||||
|---|---|
|
||||
| **Tray-App nach Installation starten** | Startet die App direkt nach Abschluss |
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Schritt: Zusammenfassung & Installation
|
||||
|
||||
Überprüfen Sie alle gewählten Einstellungen in der Zusammenfassung. Mit **„Installieren"** wird die Installation durchgeführt:
|
||||
|
||||
1. Dateien werden in den Installationsordner kopiert
|
||||
2. Der Windows-Dienst `EngineeringSync` wird registriert
|
||||
3. Der Dienst wird gestartet
|
||||
4. Autostart-Einträge werden gesetzt (falls ausgewählt)
|
||||
5. Verknüpfungen werden erstellt (falls ausgewählt)
|
||||
|
||||
Ein Fortschrittsbalken zeigt den aktuellen Status an.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 Schritt: Fertig
|
||||
|
||||
Die Installation ist abgeschlossen. Klicken Sie auf **„Schließen"** um den Setup-Assistenten zu beenden. Falls die Option „Tray-App nach Installation starten" aktiviert war, erscheint das Tray-Icon automatisch in der Windows-Taskleiste.
|
||||
|
||||
---
|
||||
|
||||
## 4. Die Tray-App im Überblick
|
||||
|
||||
Die Tray-App lebt im **Windows-Systemtray** (rechte Seite der Taskleiste, neben der Uhr).
|
||||
|
||||
```
|
||||
Taskleiste: [...] [🔔] [EngineeringSync-Icon] [Zeit]
|
||||
▲
|
||||
Rechtsklick-Menü:
|
||||
┌─────────────────────────────────┐
|
||||
│ 🔔 Ausstehende Änderungen (3) │
|
||||
│ ⚙ Projekte verwalten │
|
||||
│ ─────────────────────────────── │
|
||||
│ ✕ Beenden │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tray-Icon-Zustände
|
||||
|
||||
| Symbol | Bedeutung |
|
||||
|---|---|
|
||||
| Normal | Dienst läuft, keine ausstehenden Änderungen |
|
||||
| Mit Zahl | Anzahl der ausstehenden Änderungen, die auf Freigabe warten |
|
||||
|
||||
### Toast-Benachrichtigungen
|
||||
|
||||
Sobald neue Dateländerungen erkannt werden, erscheint automatisch eine Windows-Benachrichtigung:
|
||||
|
||||
```
|
||||
EngineeringSync
|
||||
───────────────
|
||||
Projekt "Linie 3": 2 neue Änderungen
|
||||
```
|
||||
|
||||
Ein Klick auf die Benachrichtigung öffnet direkt das Fenster „Ausstehende Änderungen".
|
||||
|
||||
---
|
||||
|
||||
## 5. Projekte verwalten
|
||||
|
||||
Über **Rechtsklick → „Projekte verwalten"** öffnet sich das Projektverwaltungs-Fenster.
|
||||
|
||||
### 5.1 Neues Projekt anlegen
|
||||
|
||||
1. Klicken Sie auf **„+ Projekt hinzufügen"**
|
||||
2. Füllen Sie die Felder aus (gleiche Felder wie im Setup-Assistenten)
|
||||
3. Bestätigen Sie mit **„Speichern"**
|
||||
|
||||
Der Dienst beginnt sofort mit der Überwachung des neuen Ordners. Ein initialer Scan vergleicht vorhandene Dateien zwischen Engineering- und Simulations-Ordner und erstellt für abweichende Dateien sofort ausstehende Änderungen.
|
||||
|
||||
### 5.2 Projekt bearbeiten
|
||||
|
||||
1. Wählen Sie ein Projekt in der Liste aus
|
||||
2. Klicken Sie auf **„Bearbeiten"** (oder Doppelklick)
|
||||
3. Ändern Sie die gewünschten Felder
|
||||
4. Bestätigen Sie mit **„Speichern"**
|
||||
|
||||
> **Hinweis:** Nach dem Speichern wird der Dateiwatcher automatisch neu gestartet. Laufende Scans werden abgebrochen und neu begonnen.
|
||||
|
||||
### 5.3 Projekt löschen
|
||||
|
||||
1. Wählen Sie ein Projekt in der Liste aus
|
||||
2. Klicken Sie auf **„Löschen"**
|
||||
3. Bestätigen Sie die Sicherheitsfrage
|
||||
|
||||
> **Achtung:** Beim Löschen eines Projekts werden auch alle zugehörigen ausstehenden Änderungen und die Datei-Revisionshistorie gelöscht. Die tatsächlichen Dateien auf der Festplatte werden **nicht** verändert.
|
||||
|
||||
### 5.4 Dateiüberwachung konfigurieren
|
||||
|
||||
#### Aktive / inaktive Projekte
|
||||
|
||||
Projekte können vorübergehend deaktiviert werden (z. B. während Wartungsarbeiten). Ein deaktiviertes Projekt erzeugt keine neuen Änderungseinträge.
|
||||
|
||||
#### Dateierweiterungen
|
||||
|
||||
Die überwachten Dateitypen können jederzeit angepasst werden. Änderungen wirken sich auf zukünftige Ereignisse aus – bestehende ausstehende Änderungen bleiben erhalten.
|
||||
|
||||
---
|
||||
|
||||
## 6. Änderungen überwachen und synchronisieren
|
||||
|
||||
### 6.1 Benachrichtigungen empfangen
|
||||
|
||||
EngineeringSync überwacht den Engineering-Ordner **in Echtzeit** mittels `FileSystemWatcher`. Folgende Ereignisse werden erfasst:
|
||||
|
||||
| Ereignistyp | Beschreibung |
|
||||
|---|---|
|
||||
| **Erstellt** | Eine neue Datei wurde im Engineering-Ordner abgelegt |
|
||||
| **Geändert** | Eine bestehende Datei wurde modifiziert (SHA-256-Prüfung bestätigt tatsächliche Änderung) |
|
||||
| **Umbenannt** | Eine Datei wurde umbenannt oder verschoben |
|
||||
| **Gelöscht** | Eine Datei wurde aus dem Engineering-Ordner entfernt |
|
||||
|
||||
#### Debouncing (Entprellung)
|
||||
|
||||
Schnelle aufeinanderfolgende Speichervorgänge (z. B. Autosave alle 5 Sekunden) werden zu einem einzigen Ereignis zusammengefasst. Das Zeitfenster beträgt **2 Sekunden** – erst nach 2 Sekunden ohne weitere Aktivität auf derselben Datei wird das Ereignis verarbeitet.
|
||||
|
||||
#### SHA-256-Prüfung
|
||||
|
||||
Bevor eine Änderung als „ausstehend" eingetragen wird, vergleicht EngineeringSync den SHA-256-Hash der Datei mit dem zuletzt gespeicherten Hash. Nur bei tatsächlich unterschiedlichem Inhalt wird eine neue Änderung erfasst. Das verhindert Falschmeldungen bei Metadaten-Änderungen (z. B. Zeitstempel-Updates).
|
||||
|
||||
### 6.2 Das Fenster „Ausstehende Änderungen"
|
||||
|
||||
Öffnen Sie das Fenster über **Rechtsklick → „Ausstehende Änderungen"** oder per Klick auf eine Toast-Benachrichtigung.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Ausstehende Änderungen │
|
||||
│ │
|
||||
│ Projekt: [Linie 3 – Roboterzelle ▼] │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┬──────┬──────────┐ │
|
||||
│ │ Datei │ Typ │ Datum │ │
|
||||
│ ├──────────────────────────────────────┼──────┼──────────┤ │
|
||||
│ │ ☑ Roboter\Arm_links.jt │ Mod. │ 14:30:22 │ │
|
||||
│ │ ☑ Roboter\Arm_rechts.jt │ Neu │ 14:31:05 │ │
|
||||
│ │ ☐ Basis\Grundplatte.jt │ Mod. │ 09:15:44 │ │
|
||||
│ └──────────────────────────────────────┴──────┴──────────┘ │
|
||||
│ │
|
||||
│ [Alle wählen] [Keine] [✔ Sync] [✖ Ignorieren] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Projektauswahl
|
||||
|
||||
Falls mehrere Projekte konfiguriert sind, wählen Sie das gewünschte Projekt aus dem Dropdown-Menü. Die Tabelle aktualisiert sich automatisch per SignalR – neue Änderungen erscheinen sofort ohne manuelles Neuladen.
|
||||
|
||||
#### Änderungstypen
|
||||
|
||||
| Symbol | Typ | Bedeutung |
|
||||
|---|---|---|
|
||||
| **Neu** | Created | Datei existiert noch nicht im Simulations-Ordner |
|
||||
| **Mod.** | Modified | Datei wurde inhaltlich geändert (anderer SHA-256) |
|
||||
| **Umbn.** | Renamed | Datei wurde umbenannt (Alter Name wird im Sim-Ordner entfernt) |
|
||||
| **Gel.** | Deleted | Datei wurde aus dem Engineering-Ordner gelöscht |
|
||||
|
||||
### 6.3 Änderungen synchronisieren
|
||||
|
||||
1. Wählen Sie die gewünschten Änderungen per Checkbox aus (oder **„Alle wählen"**)
|
||||
2. Klicken Sie auf **„✔ Sync"**
|
||||
|
||||
EngineeringSync verarbeitet jede ausgewählte Änderung:
|
||||
|
||||
**Bei „Erstellt" oder „Geändert":**
|
||||
```
|
||||
1. Backup der bestehenden Simulationsdatei erstellen (falls Backup aktiviert)
|
||||
2. Zielverzeichnis erstellen (falls nicht vorhanden)
|
||||
3. Datei aus Engineering-Ordner in Simulations-Ordner kopieren
|
||||
4. Änderung als "Synchronisiert" markieren
|
||||
```
|
||||
|
||||
**Bei „Gelöscht":**
|
||||
```
|
||||
1. Backup der Simulationsdatei erstellen (falls Backup aktiviert)
|
||||
2. Datei aus Simulations-Ordner entfernen
|
||||
3. Änderung als "Synchronisiert" markieren
|
||||
```
|
||||
|
||||
**Bei „Umbenannt":**
|
||||
```
|
||||
1. Neue Datei unter neuem Namen in Simulations-Ordner kopieren
|
||||
2. Alte Datei aus Simulations-Ordner entfernen
|
||||
3. Änderung als "Synchronisiert" markieren
|
||||
```
|
||||
|
||||
> **Tipp:** Sie müssen nicht auf alle Änderungen gleichzeitig reagieren. Wählen Sie gezielt die Dateien aus, die für die aktuell laufende Simulation relevant sind.
|
||||
|
||||
### 6.4 Änderungen ignorieren
|
||||
|
||||
Änderungen, die für die Simulation nicht relevant sind (z. B. temporäre Hilfsdateien, Arbeitsversionen), können ignoriert werden:
|
||||
|
||||
1. Wählen Sie die irrelevanten Änderungen per Checkbox aus
|
||||
2. Klicken Sie auf **„✖ Ignorieren"**
|
||||
|
||||
Ignorierte Änderungen werden in der Hauptliste ausgeblendet und in der **Historienliste** mit dem Status „Ignoriert" gespeichert. Sie haben **keinen Einfluss** auf den Simulations-Ordner.
|
||||
|
||||
### 6.5 Änderungshistorie
|
||||
|
||||
Die Historienliste zeigt die letzten 100 bearbeiteten Änderungen (synchronisiert oder ignoriert) mit Zeitstempel und Bearbeiter-Status. Die Ansicht dient zur Nachvollziehbarkeit: Wann wurde welche Datei zuletzt synchronisiert?
|
||||
|
||||
---
|
||||
|
||||
## 7. Backup-Funktion
|
||||
|
||||
Das Backup-System schützt Simulationsdateien vor ungewolltem Überschreiben.
|
||||
|
||||
### Wann wird ein Backup erstellt?
|
||||
|
||||
Ein Backup wird **unmittelbar vor** dem Überschreiben einer Datei erstellt, also beim Synchronisieren einer „Geändert"-, „Erstellt"- oder „Gelöscht"-Änderung, wenn die Datei bereits im Simulations-Ordner existiert.
|
||||
|
||||
### Backup-Dateiname
|
||||
|
||||
Das Namensschema: `{Originalname}_{JJJJMMTT_HHMMSS}.bak`
|
||||
|
||||
Beispiele:
|
||||
```
|
||||
Roboter_Arm.jt_20260315_143022.bak
|
||||
Grundplatte.cojt_20260316_091500.bak
|
||||
```
|
||||
|
||||
### Backup-Speicherort
|
||||
|
||||
| Modus | Speicherort |
|
||||
|---|---|
|
||||
| Gleicher Ordner | Backup liegt neben der Simulationsdatei |
|
||||
| Eigener Ordner | Alle Backups in einem zentralen Verzeichnis (Unterordnerstruktur bleibt erhalten) |
|
||||
|
||||
### Aufbewahrungsregel
|
||||
|
||||
Bei aktivierter Aufbewahrungsregel (z. B. max. 5 Backups pro Datei) wird **nach** dem Erstellen des neuen Backups geprüft, ob ältere Backups gelöscht werden müssen. Die Backups werden nach Erstellungsdatum sortiert – die ältesten werden zuerst gelöscht.
|
||||
|
||||
### Backup manuell wiederherstellen
|
||||
|
||||
EngineeringSync bietet keine automatische Wiederherstellungsfunktion. Um eine Backup-Datei manuell wiederherzustellen:
|
||||
|
||||
1. Navigieren Sie zum Backup-Speicherort im Windows Explorer
|
||||
2. Benennen Sie die `.bak`-Datei um (entfernen Sie den Zeitstempel und `.bak`)
|
||||
3. Kopieren Sie die Datei an den gewünschten Ort
|
||||
|
||||
---
|
||||
|
||||
## 8. Windows-Dienst verwalten
|
||||
|
||||
Der **EngineeringSync-Dienst** läuft als Windows-Dienst und ist unabhängig von der Tray-App.
|
||||
|
||||
### Dienst-Status prüfen
|
||||
|
||||
Öffnen Sie den Windows Taskmanager (Strg+Umsch+Esc) → Reiter **„Dienste"**, oder:
|
||||
|
||||
```powershell
|
||||
# PowerShell
|
||||
Get-Service EngineeringSync
|
||||
|
||||
# Ausgabe:
|
||||
Status Name DisplayName
|
||||
------ ---- -----------
|
||||
Running EngineeringSync EngineeringSync
|
||||
```
|
||||
|
||||
### Dienst manuell steuern
|
||||
|
||||
```powershell
|
||||
# Dienst starten
|
||||
Start-Service EngineeringSync
|
||||
|
||||
# Dienst stoppen
|
||||
Stop-Service EngineeringSync
|
||||
|
||||
# Dienst neu starten
|
||||
Restart-Service EngineeringSync
|
||||
```
|
||||
|
||||
Alternativ über die **Windows Dienste-Verwaltung** (Win+R → `services.msc`).
|
||||
|
||||
### API-Endpunkt
|
||||
|
||||
Der Dienst stellt eine lokale REST-API unter `http://localhost:5050` bereit. Die Tray-App kommuniziert über diese API. Sie können die API auch direkt im Browser testen:
|
||||
|
||||
| Endpunkt | Beschreibung |
|
||||
|---|---|
|
||||
| `GET /api/projects` | Alle konfigurierten Projekte |
|
||||
| `GET /api/changes/{projectId}` | Ausstehende Änderungen eines Projekts |
|
||||
| `GET /api/changes/{projectId}/history` | Letzte 100 bearbeitete Änderungen |
|
||||
|
||||
> **Hinweis:** Der API-Endpunkt ist nur lokal erreichbar (`localhost`). Er ist nicht durch Authentifizierung gesichert – stellen Sie sicher, dass kein unberechtigter Netzwerkzugang besteht.
|
||||
|
||||
---
|
||||
|
||||
## 9. Deinstallation
|
||||
|
||||
### Über die Windows Einstellungen
|
||||
|
||||
1. Öffnen Sie **Einstellungen → Apps → Installierte Apps**
|
||||
2. Suchen Sie nach „EngineeringSync"
|
||||
3. Klicken Sie auf **„Deinstallieren"**
|
||||
|
||||
### Manuell
|
||||
|
||||
Führen Sie die Setup-Datei mit dem Parameter `/uninstall` aus:
|
||||
|
||||
```cmd
|
||||
EngineeringSync-Setup.exe /uninstall
|
||||
```
|
||||
|
||||
### Was wird bei der Deinstallation entfernt?
|
||||
|
||||
| Element | Wird entfernt? |
|
||||
|---|---|
|
||||
| Installationsdateien | ✅ Ja |
|
||||
| Windows-Dienst | ✅ Ja (wird gestoppt und gelöscht) |
|
||||
| Autostart-Eintrag | ✅ Ja (nur der EngineeringSync-Eintrag) |
|
||||
| Desktop-Verknüpfung | ✅ Ja |
|
||||
| Startmenü-Eintrag | ✅ Ja |
|
||||
| SQLite-Datenbank | ❌ Nein (Konfiguration und Historie bleiben erhalten) |
|
||||
| Backup-Dateien | ❌ Nein |
|
||||
| Engineering-/Simulations-Ordner | ❌ Nein (niemals) |
|
||||
|
||||
> Die SQLite-Datenbank (`engineeringsync.db`) verbleibt im Installationsordner und kann manuell gelöscht werden.
|
||||
|
||||
---
|
||||
|
||||
## 10. Technische Architektur
|
||||
|
||||
Dieser Abschnitt richtet sich an technische Nutzer und Administratoren.
|
||||
|
||||
### Komponenten
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ EngineeringSync │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌───────────────────────────┐ │
|
||||
│ │ TrayApp (WPF) │◄────│ Service (Worker Service) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Tray-Icon │ │ • FileSystemWatcher │ │
|
||||
│ │ • Änderungs- │ │ • Debounce-Channel │ │
|
||||
│ │ fenster │ │ • SHA-256-Hashing │ │
|
||||
│ │ • Projekt- │ │ • SyncManager │ │
|
||||
│ │ verwaltung │ │ • REST API (Port 5050) │ │
|
||||
│ │ │ │ • SignalR Hub │ │
|
||||
│ └────────┬─────────┘ └──────────────┬────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ HTTP (REST + SignalR) │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ SQLite-Datenbank (WAL-Modus) │ │
|
||||
│ │ • ProjectConfig • FileRevision • PendingChange │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Datenbankschema
|
||||
|
||||
**ProjectConfig** – Projektkonfigurationen
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `Id` | GUID | Eindeutiger Bezeichner |
|
||||
| `Name` | Text | Anzeigename des Projekts |
|
||||
| `EngineeringPath` | Text | Überwachter Quellordner |
|
||||
| `SimulationPath` | Text | Sync-Zielordner |
|
||||
| `FileExtensions` | Text | Überwachte Dateitypen (komma-getrennt oder `*`) |
|
||||
| `IsActive` | Bool | Überwachung aktiv/inaktiv |
|
||||
| `BackupEnabled` | Bool | Backup aktiviert |
|
||||
| `BackupPath` | Text? | Optionaler Backup-Pfad |
|
||||
| `MaxBackupsPerFile` | Int | Max. Backups pro Datei (0 = unbegrenzt) |
|
||||
|
||||
**FileRevision** – Zuletzt bekannte Dateiversion
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `Id` | GUID | Eindeutiger Bezeichner |
|
||||
| `ProjectId` | GUID | Verweis auf Projekt |
|
||||
| `RelativePath` | Text | Relativer Dateipfad |
|
||||
| `FileHash` | Text | SHA-256-Hash des Dateiinhalts |
|
||||
| `Size` | Long | Dateigröße in Bytes |
|
||||
| `LastModified` | DateTime | Letzte Änderungszeit |
|
||||
|
||||
**PendingChange** – Ausstehende oder bearbeitete Änderungen
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `Id` | GUID | Eindeutiger Bezeichner |
|
||||
| `ProjectId` | GUID | Verweis auf Projekt |
|
||||
| `RelativePath` | Text | Relativer Dateipfad |
|
||||
| `ChangeType` | Enum | Created / Modified / Renamed / Deleted |
|
||||
| `OldRelativePath` | Text? | Alter Pfad (nur bei Renamed) |
|
||||
| `Status` | Enum | Pending / Synced / Ignored |
|
||||
| `CreatedAt` | DateTime | Zeitpunkt der Erkennung |
|
||||
| `SyncedAt` | DateTime? | Zeitpunkt der Bearbeitung |
|
||||
|
||||
### Kommunikation (SignalR)
|
||||
|
||||
Die Tray-App empfängt Echtzeit-Benachrichtigungen über SignalR (WebSockets):
|
||||
|
||||
| Methode | Parameter | Auslöser |
|
||||
|---|---|---|
|
||||
| `ReceiveChangeNotification` | `projectId, projectName, count` | Neue ausstehende Änderungen erkannt |
|
||||
| `ProjectConfigChanged` | – | Projekt wurde erstellt, geändert oder gelöscht |
|
||||
|
||||
### Datenbankdatei
|
||||
|
||||
```
|
||||
%ProgramFiles%\EngineeringSync\engineeringsync.db
|
||||
```
|
||||
|
||||
Die Datenbank läuft im **WAL-Modus** (Write-Ahead Logging), was gleichzeitige Lese- und Schreibzugriffe ohne Sperren ermöglicht.
|
||||
|
||||
---
|
||||
|
||||
## 11. Fehlerbehebung & FAQ
|
||||
|
||||
### Die Tray-App zeigt „Dienst nicht erreichbar"
|
||||
|
||||
**Ursache:** Der EngineeringSync-Dienst läuft nicht oder Port 5050 ist belegt.
|
||||
|
||||
**Lösung:**
|
||||
1. Prüfen Sie den Dienst-Status: `Get-Service EngineeringSync`
|
||||
2. Starten Sie den Dienst: `Start-Service EngineeringSync`
|
||||
3. Falls Port 5050 belegt ist: `netstat -ano | findstr :5050`
|
||||
|
||||
---
|
||||
|
||||
### Änderungen werden nicht erkannt
|
||||
|
||||
**Mögliche Ursachen und Lösungen:**
|
||||
|
||||
| Ursache | Lösung |
|
||||
|---|---|
|
||||
| Projekt ist inaktiv | In Projektverwaltung das Projekt aktivieren |
|
||||
| Falsche Dateierweiterung konfiguriert | Erweiterungen prüfen oder „Alle Dateitypen" wählen |
|
||||
| Engineering-Pfad nicht erreichbar | Pfad im Explorer prüfen, Netzlaufwerk verbunden? |
|
||||
| Dienst gestoppt | `Start-Service EngineeringSync` |
|
||||
|
||||
---
|
||||
|
||||
### Sehr viele Änderungen auf einmal (z. B. nach Bulk-Copy)
|
||||
|
||||
Bei vielen gleichzeitigen Dateiereignissen kann der interne Event-Puffer des Datei-Watchers überlaufen. EngineeringSync startet in diesem Fall automatisch einen vollständigen Re-Scan des Engineering-Ordners. Dieser Vorgang kann bei großen Ordnern mehrere Minuten dauern.
|
||||
|
||||
---
|
||||
|
||||
### Eine Datei ist gesperrt und wird nicht synchronisiert
|
||||
|
||||
CAD-Anwendungen sperren Dateien während der Bearbeitung exklusiv. EngineeringSync versucht beim Erkennen einer gesperrten Datei automatisch bis zu **3 Wiederholungen** mit zunehmendem Warteintervall (2s, 4s, 6s). Wenn die Datei nach allen Versuchen noch gesperrt ist, wird das Ereignis in den Logs vermerkt und beim nächsten periodischen Scan erneut versucht.
|
||||
|
||||
**Empfehlung:** Synchronisieren Sie Dateien erst, wenn die CAD-Anwendung die Datei freigegeben hat (Datei geschlossen oder gespeichert).
|
||||
|
||||
---
|
||||
|
||||
### Wo liegen die Log-Dateien?
|
||||
|
||||
```
|
||||
%ProgramFiles%\EngineeringSync\logs\
|
||||
```
|
||||
|
||||
Oder im Windows Ereignisprotokoll unter:
|
||||
**Ereignisanzeige → Windows-Protokolle → Anwendung** (Quelle: `EngineeringSync`)
|
||||
|
||||
---
|
||||
|
||||
### Kann ich mehrere Engineering-Ordner mit einem Simulations-Ordner verbinden?
|
||||
|
||||
Ja – legen Sie pro Engineering-Ordner ein eigenes Projekt an. Mehrere Projekte können auf denselben Simulations-Ordner zeigen, solange sich die relativen Pfade nicht überschneiden.
|
||||
|
||||
---
|
||||
|
||||
### Was passiert, wenn ich eine Datei im Simulations-Ordner manuell bearbeite?
|
||||
|
||||
EngineeringSync überwacht **nur** den Engineering-Quellordner. Änderungen im Simulations-Ordner werden nicht erfasst. Bei der nächsten Synchronisation einer entsprechenden Datei wird die manuell bearbeitete Version im Simulations-Ordner durch ein Backup gesichert und dann überschrieben.
|
||||
|
||||
---
|
||||
|
||||
### Wie hoch ist der Ressourcenverbrauch des Dienstes?
|
||||
|
||||
Im Normalbetrieb (Überwachung aktiv, keine Aktivität):
|
||||
- CPU: < 0,1 %
|
||||
- RAM: ca. 50–80 MB
|
||||
|
||||
Bei einem vollständigen Re-Scan großer Ordner (>10.000 Dateien) kann die CPU-Last kurzzeitig auf 10–20 % steigen.
|
||||
|
||||
---
|
||||
|
||||
### Kann ich die Datenbank auf einem Netzlaufwerk ablegen?
|
||||
|
||||
Nein. Die SQLite-Datenbank muss auf einem lokalen Laufwerk liegen. SQLite und Netzlaufwerke sind nicht kompatibel (Sperr-Mechanismus funktioniert über SMB nicht zuverlässig).
|
||||
|
||||
---
|
||||
|
||||
*Dieses Handbuch bezieht sich auf EngineeringSync Version 1.0.0.*
|
||||
*Für Fragen und Support wenden Sie sich an den Projektverantwortlichen.*
|
||||
967
docs/superpowers/plans/2026-03-26-backup-settings.md
Normal file
967
docs/superpowers/plans/2026-03-26-backup-settings.md
Normal file
@@ -0,0 +1,967 @@
|
||||
# Backup-Einstellungen pro Projekt – Implementierungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Pro-Projekt konfigurierbare Backup-Einstellungen (an/aus, eigener Ordner, Aufbewahrungsregel) im Setup-Wizard und TrayApp.
|
||||
|
||||
**Architecture:** Drei neue Felder direkt in `ProjectConfig` (BackupEnabled, BackupPath, MaxBackupsPerFile). `SyncManager.BackupFile` wird zur Instanzmethode mit cross-volume-sicherem Verschieben und automatischem Cleanup. Setup-Wizard erhält eine neue `BackupOptionsPage`, TrayApp eine neue BACKUP-Sektion im `ProjectManagementWindow`.
|
||||
|
||||
**Tech Stack:** .NET 10, EF Core 10 + SQLite, ASP.NET Core Minimal API, WPF + CommunityToolkit.Mvvm, `OpenFolderDialog` (Microsoft.Win32)
|
||||
|
||||
---
|
||||
|
||||
## Dateiübersicht
|
||||
|
||||
| Aktion | Datei |
|
||||
|--------|-------|
|
||||
| Modify | `EngineeringSync.Domain/Entities/ProjectConfig.cs` |
|
||||
| Modify | `EngineeringSync.Infrastructure/AppDbContext.cs` |
|
||||
| **Generate** | `EngineeringSync.Infrastructure/Migrations/..._AddBackupSettings.cs` (via `dotnet ef`) |
|
||||
| Modify | `EngineeringSync.Service/Services/SyncManager.cs` |
|
||||
| Modify | `EngineeringSync.Service/Models/ApiModels.cs` |
|
||||
| Modify | `EngineeringSync.Service/Api/ProjectsApi.cs` |
|
||||
| Modify | `EngineeringSync.Service/Program.cs` |
|
||||
| Modify | `EngineeringSync.Setup/ViewModels/WizardState.cs` |
|
||||
| **Create** | `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml` |
|
||||
| **Create** | `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs` |
|
||||
| Modify | `EngineeringSync.Setup/ViewModels/WizardViewModel.cs` |
|
||||
| Modify | `EngineeringSync.Setup/Services/InstallerService.cs` |
|
||||
| Modify | `EngineeringSync.Setup/Views/Pages/SummaryPage.xaml` |
|
||||
| Modify | `EngineeringSync.TrayApp/Services/ApiClient.cs` |
|
||||
| Modify | `EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs` |
|
||||
| Modify | `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml` |
|
||||
| Modify | `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs` |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Domain – Backup-Felder in ProjectConfig
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Domain/Entities/ProjectConfig.cs`
|
||||
|
||||
- [ ] **Schritt 1: Drei Felder nach `IsActive` einfügen**
|
||||
|
||||
```csharp
|
||||
// In ProjectConfig.cs – nach "public bool IsActive" einfügen:
|
||||
public bool BackupEnabled { get; set; } = true;
|
||||
public string? BackupPath { get; set; } = null; // null = gleicher Ordner
|
||||
public int MaxBackupsPerFile { get; set; } = 0; // 0 = unbegrenzt
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Domain/EngineeringSync.Domain.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded` ohne Fehler.
|
||||
|
||||
- [ ] **Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Domain/Entities/ProjectConfig.cs
|
||||
git commit -m "feat(domain): add BackupEnabled, BackupPath, MaxBackupsPerFile to ProjectConfig"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Infrastructure – EF-Migration
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Infrastructure/AppDbContext.cs`
|
||||
- Generate: `EngineeringSync.Infrastructure/Migrations/..._AddBackupSettings.cs`
|
||||
|
||||
- [ ] **Schritt 1: HasDefaultValue in AppDbContext eintragen**
|
||||
|
||||
In `AppDbContext.cs`, die bestehende `modelBuilder.Entity<ProjectConfig>(e => {...})` Konfiguration erweitern:
|
||||
|
||||
```csharp
|
||||
modelBuilder.Entity<ProjectConfig>(e =>
|
||||
{
|
||||
e.HasKey(p => p.Id);
|
||||
e.Property(p => p.Name).IsRequired().HasMaxLength(200);
|
||||
e.Property(p => p.EngineeringPath).IsRequired();
|
||||
e.Property(p => p.SimulationPath).IsRequired();
|
||||
// NEU: Standardwerte für Backup-Felder (für bestehende Datenbankzeilen)
|
||||
e.Property(p => p.BackupEnabled).HasDefaultValue(true);
|
||||
e.Property(p => p.BackupPath).HasDefaultValue(null);
|
||||
e.Property(p => p.MaxBackupsPerFile).HasDefaultValue(0);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Migration generieren**
|
||||
|
||||
```bash
|
||||
cd D:/001_Projekte/021_KON-SIM
|
||||
dotnet ef migrations add AddBackupSettingsToProjectConfig \
|
||||
--project EngineeringSync.Infrastructure \
|
||||
--startup-project EngineeringSync.Service
|
||||
```
|
||||
|
||||
Erwartung: Neue Migrationsdatei in `EngineeringSync.Infrastructure/Migrations/` mit drei `AddColumn`-Aufrufen für `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile`.
|
||||
|
||||
- [ ] **Schritt 3: Generierte Migration kurz prüfen**
|
||||
|
||||
Die generierte `.cs`-Datei öffnen und sicherstellen, dass `defaultValue: true` (BackupEnabled) und `defaultValue: 0` (MaxBackupsPerFile) enthalten sind. Falls Werte fehlen: manuell ergänzen.
|
||||
|
||||
- [ ] **Schritt 4: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Infrastructure/EngineeringSync.Infrastructure.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded`.
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Infrastructure/AppDbContext.cs
|
||||
git add EngineeringSync.Infrastructure/Migrations/
|
||||
git commit -m "feat(infra): migration AddBackupSettingsToProjectConfig"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Service – SyncManager refaktorieren
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Service/Services/SyncManager.cs`
|
||||
|
||||
- [ ] **Schritt 1: `ProcessChangeAsync` – `static` entfernen und beide BackupFile-Aufrufe anpassen**
|
||||
|
||||
Die Methode `ProcessChangeAsync` ist aktuell `static` (Zeile 47). Da sie gleich eine Instanzmethode (`BackupFile`) aufrufen soll, muss `static` entfernt werden:
|
||||
|
||||
```csharp
|
||||
// Vorher:
|
||||
private static async Task ProcessChangeAsync(PendingChange change, CancellationToken ct)
|
||||
// Nachher:
|
||||
private async Task ProcessChangeAsync(PendingChange change, CancellationToken ct)
|
||||
```
|
||||
|
||||
Dann beide BackupFile-Aufrufe anpassen:
|
||||
- Zeile ~57: im `Deleted`-Branch: `BackupFile(targetPath);` → `BackupFile(targetPath, project);`
|
||||
- Zeile ~69: im Overwrite-Branch: `BackupFile(targetPath);` → `BackupFile(targetPath, project);`
|
||||
|
||||
- [ ] **Schritt 2: `BackupFile` von `static` zur Instanzmethode umbauen**
|
||||
|
||||
```csharp
|
||||
private void BackupFile(string targetPath, ProjectConfig project)
|
||||
{
|
||||
if (!project.BackupEnabled)
|
||||
return;
|
||||
|
||||
var backupDir = project.BackupPath ?? Path.GetDirectoryName(targetPath)!;
|
||||
Directory.CreateDirectory(backupDir);
|
||||
|
||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(targetPath);
|
||||
var ext = Path.GetExtension(targetPath);
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var backupPath = Path.Combine(backupDir, $"{nameWithoutExt}_{timestamp}{ext}.bak");
|
||||
|
||||
// Cross-volume-sicheres Verschieben (z.B. Netzlaufwerke, andere Laufwerksbuchstaben)
|
||||
try
|
||||
{
|
||||
File.Move(targetPath, backupPath);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
File.Copy(targetPath, backupPath, overwrite: false);
|
||||
File.Delete(targetPath);
|
||||
}
|
||||
|
||||
if (project.MaxBackupsPerFile > 0)
|
||||
CleanupOldBackups(backupDir, nameWithoutExt, ext, project.MaxBackupsPerFile);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: `CleanupOldBackups` als private Instanzmethode hinzufügen**
|
||||
|
||||
```csharp
|
||||
private void CleanupOldBackups(string backupDir, string baseName, string ext, int max)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pattern = $"{baseName}_*{ext}.bak";
|
||||
var backups = Directory.GetFiles(backupDir, pattern)
|
||||
.OrderBy(f => Path.GetFileName(f)) // yyyyMMdd_HHmmss → lexikografisch = zeitlich
|
||||
.ToList();
|
||||
|
||||
while (backups.Count > max)
|
||||
{
|
||||
File.Delete(backups[0]);
|
||||
backups.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Cleanup alter Backups fehlgeschlagen in {Dir}", backupDir);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded`. Bei Compiler-Fehler `static member cannot be used with instance receiver`: sicherstellen dass `BackupFile` nicht mehr `static` ist.
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Service/Services/SyncManager.cs
|
||||
git commit -m "feat(service): BackupFile als Instanzmethode mit cross-volume-Support und CleanupOldBackups"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Service – API-Modelle und ProjectsApi
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Service/Models/ApiModels.cs`
|
||||
- Modify: `EngineeringSync.Service/Api/ProjectsApi.cs`
|
||||
|
||||
- [ ] **Schritt 1: API-Modelle um Backup-Felder erweitern**
|
||||
|
||||
`CreateProjectRequest` und `UpdateProjectRequest` in `ApiModels.cs` ersetzen durch:
|
||||
|
||||
```csharp
|
||||
public record CreateProjectRequest(
|
||||
string Name,
|
||||
string EngineeringPath,
|
||||
string SimulationPath,
|
||||
string FileExtensions,
|
||||
bool IsActive = true,
|
||||
bool BackupEnabled = true,
|
||||
string? BackupPath = null,
|
||||
int MaxBackupsPerFile = 0
|
||||
);
|
||||
|
||||
public record UpdateProjectRequest(
|
||||
string Name,
|
||||
string EngineeringPath,
|
||||
string SimulationPath,
|
||||
string FileExtensions,
|
||||
bool IsActive,
|
||||
bool BackupEnabled = true,
|
||||
string? BackupPath = null,
|
||||
int MaxBackupsPerFile = 0
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: ProjectsApi – neue Felder beim Erstellen mappen**
|
||||
|
||||
In `ProjectsApi.cs`, den `MapPost`-Handler: beim Erstellen der `ProjectConfig` die neuen Felder setzen:
|
||||
|
||||
```csharp
|
||||
var project = new ProjectConfig
|
||||
{
|
||||
Name = req.Name,
|
||||
EngineeringPath = req.EngineeringPath,
|
||||
SimulationPath = req.SimulationPath,
|
||||
FileExtensions = req.FileExtensions,
|
||||
IsActive = req.IsActive,
|
||||
BackupEnabled = req.BackupEnabled, // NEU
|
||||
BackupPath = req.BackupPath, // NEU
|
||||
MaxBackupsPerFile = req.MaxBackupsPerFile // NEU
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: ProjectsApi – neue Felder beim Aktualisieren mappen**
|
||||
|
||||
Im `MapPut`-Handler, nach den bestehenden Zuweisungen ergänzen:
|
||||
|
||||
```csharp
|
||||
project.Name = req.Name;
|
||||
project.EngineeringPath = req.EngineeringPath;
|
||||
project.SimulationPath = req.SimulationPath;
|
||||
project.FileExtensions = req.FileExtensions;
|
||||
project.IsActive = req.IsActive;
|
||||
project.BackupEnabled = req.BackupEnabled; // NEU
|
||||
project.BackupPath = req.BackupPath; // NEU
|
||||
project.MaxBackupsPerFile = req.MaxBackupsPerFile; // NEU
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded`.
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Service/Models/ApiModels.cs EngineeringSync.Service/Api/ProjectsApi.cs
|
||||
git commit -m "feat(service): Backup-Felder in API-Modelle und ProjectsApi"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Service – Program.cs ProcessFirstRunConfigAsync
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Service/Program.cs`
|
||||
|
||||
- [ ] **Schritt 1: Backup-Abschnitt aus firstrun-config.json lesen**
|
||||
|
||||
In `Program.cs`, in `ProcessFirstRunConfigAsync()`, direkt nach dem Auslesen von `watchAllFiles` (ca. Zeile 58) folgenden Block einfügen:
|
||||
|
||||
```csharp
|
||||
// Backup-Einstellungen (optional, mit Standardwerten für Rückwärtskompatibilität)
|
||||
var backupEnabled = true;
|
||||
string? backupPath = null;
|
||||
var maxBackupsPerFile = 0;
|
||||
|
||||
if (root.RootElement.TryGetProperty("Backup", out var backupElement))
|
||||
{
|
||||
if (backupElement.TryGetProperty("BackupEnabled", out var be))
|
||||
backupEnabled = be.GetBoolean();
|
||||
if (backupElement.TryGetProperty("BackupPath", out var bp) &&
|
||||
bp.ValueKind != JsonValueKind.Null)
|
||||
backupPath = bp.GetString();
|
||||
if (backupElement.TryGetProperty("MaxBackupsPerFile", out var mb))
|
||||
maxBackupsPerFile = mb.GetInt32();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Backup-Felder beim Erstellen der ProjectConfig setzen**
|
||||
|
||||
Im selben `ProcessFirstRunConfigAsync`, beim `new ProjectConfig { ... }` Block die neuen Felder ergänzen:
|
||||
|
||||
```csharp
|
||||
var project = new EngineeringSync.Domain.Entities.ProjectConfig
|
||||
{
|
||||
Name = projectName,
|
||||
EngineeringPath = engineeringPath,
|
||||
SimulationPath = simulationPath,
|
||||
FileExtensions = fileExtensions,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
BackupEnabled = backupEnabled, // NEU
|
||||
BackupPath = backupPath, // NEU
|
||||
MaxBackupsPerFile = maxBackupsPerFile // NEU
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded`.
|
||||
|
||||
- [ ] **Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Service/Program.cs
|
||||
git commit -m "feat(service): Backup-Felder aus firstrun-config.json lesen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Setup-Wizard – WizardState
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Setup/ViewModels/WizardState.cs`
|
||||
|
||||
- [ ] **Schritt 1: Vier neue Properties im Abschnitt „Erstes Projekt" hinzufügen**
|
||||
|
||||
```csharp
|
||||
// --- Backup-Einstellungen ---
|
||||
[ObservableProperty] private bool _backupEnabled = true;
|
||||
[ObservableProperty] private bool _backupUseCustomPath = false;
|
||||
[ObservableProperty] private string _backupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _maxBackupsPerFile = 0;
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Setup/ViewModels/WizardState.cs
|
||||
git commit -m "feat(setup): Backup-Properties in WizardState"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Setup-Wizard – BackupOptionsPage erstellen
|
||||
|
||||
**Files:**
|
||||
- Create: `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml`
|
||||
- Create: `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs`
|
||||
|
||||
- [ ] **Schritt 1: BackupOptionsPage.xaml erstellen**
|
||||
|
||||
```xml
|
||||
<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="" 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="" 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>
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: BackupOptionsPage.xaml.cs erstellen**
|
||||
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj
|
||||
```
|
||||
Erwartung: `Build succeeded`. Häufiger Fehler: fehlendes `x:Class`-Attribut oder falsche Namespace-Angaben → prüfen ob `xmlns:local` und `x:Class` übereinstimmen.
|
||||
|
||||
- [ ] **Schritt 4: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml
|
||||
git add EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs
|
||||
git commit -m "feat(setup): BackupOptionsPage erstellen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Setup-Wizard – WizardViewModel und InstallerService
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.Setup/ViewModels/WizardViewModel.cs`
|
||||
- Modify: `EngineeringSync.Setup/Services/InstallerService.cs`
|
||||
- Modify: `EngineeringSync.Setup/Views/Pages/SummaryPage.xaml`
|
||||
|
||||
- [ ] **Schritt 1: WizardViewModel – neuen Schritt und neue Page einfügen**
|
||||
|
||||
In `WizardViewModel.cs`, die `Steps`-Liste: neuen Eintrag an Index 3 einfügen:
|
||||
|
||||
```csharp
|
||||
public ObservableCollection<WizardStep> Steps { get; } =
|
||||
[
|
||||
new("Willkommen", "\uE80F"),
|
||||
new("Installation", "\uE7B7"),
|
||||
new("Erstes Projekt", "\uE8B7"),
|
||||
new("Backup", "\uE72E"), // NEU – Index 3
|
||||
new("Optionen", "\uE713"),
|
||||
new("Zusammenfassung","\uE8A9"),
|
||||
new("Installation", "\uE896"),
|
||||
new("Fertig", "\uE930"),
|
||||
];
|
||||
```
|
||||
|
||||
Und `_pageFactories`: `() => new BackupOptionsPage(this)` an Index 3 einfügen:
|
||||
|
||||
```csharp
|
||||
_pageFactories =
|
||||
[
|
||||
() => new WelcomePage(this),
|
||||
() => new InstallPathPage(this),
|
||||
() => new FirstProjectPage(this),
|
||||
() => new BackupOptionsPage(this), // NEU – Index 3
|
||||
() => new ServiceOptionsPage(this),
|
||||
() => new SummaryPage(this),
|
||||
() => new InstallingPage(this, _installer),
|
||||
() => new CompletionPage(this),
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: InstallerService – Backup-Abschnitt in firstrun-config.json schreiben**
|
||||
|
||||
In `InstallerService.cs`, in der Methode `WriteFirstRunConfig()`:
|
||||
|
||||
Das anonyme Objekt `config` erweitern um einen `Backup`-Abschnitt:
|
||||
|
||||
```csharp
|
||||
var config = new
|
||||
{
|
||||
FirstRun = new
|
||||
{
|
||||
ProjectName = state.ProjectName,
|
||||
EngineeringPath = state.EngineeringPath,
|
||||
SimulationPath = state.SimulationPath,
|
||||
FileExtensions = state.WatchAllFiles ? "*" : state.FileExtensions,
|
||||
WatchAllFiles = state.WatchAllFiles
|
||||
},
|
||||
Backup = new // NEU
|
||||
{
|
||||
BackupEnabled = state.BackupEnabled,
|
||||
BackupPath = state.BackupUseCustomPath ? state.BackupCustomPath : (string?)null,
|
||||
MaxBackupsPerFile = state.MaxBackupsPerFile
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: SummaryPage.xaml – Backup-Karte hinzufügen**
|
||||
|
||||
In `SummaryPage.xaml`, nach dem bestehenden „Optionen"-Block, vor dem abschließenden `</StackPanel>` des `ScrollViewer`, folgende Karte einfügen:
|
||||
|
||||
```xml
|
||||
<!-- Backup -->
|
||||
<Border Style="{StaticResource InfoCardStyle}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="" 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>
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj
|
||||
```
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.Setup/ViewModels/WizardViewModel.cs
|
||||
git add EngineeringSync.Setup/Services/InstallerService.cs
|
||||
git add EngineeringSync.Setup/Views/Pages/SummaryPage.xaml
|
||||
git commit -m "feat(setup): BackupOptionsPage in Wizard einbinden, InstallerService und SummaryPage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: TrayApp – ApiClient-DTOs
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.TrayApp/Services/ApiClient.cs`
|
||||
|
||||
- [ ] **Schritt 1: DTOs um Backup-Felder erweitern**
|
||||
|
||||
`CreateProjectDto` und `UpdateProjectDto` in `ApiClient.cs` ersetzen:
|
||||
|
||||
```csharp
|
||||
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
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.TrayApp/Services/ApiClient.cs
|
||||
git commit -m "feat(trayapp): Backup-Felder in CreateProjectDto und UpdateProjectDto"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: TrayApp – ProjectManagementViewModel
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs`
|
||||
|
||||
- [ ] **Schritt 1: Vier neue ObservableProperty-Felder hinzufügen**
|
||||
|
||||
Nach `_editIsActive` einfügen:
|
||||
|
||||
```csharp
|
||||
[ObservableProperty] private bool _editBackupEnabled = true;
|
||||
[ObservableProperty] private bool _editBackupUseCustomPath = false;
|
||||
[ObservableProperty] private string _editBackupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _editMaxBackupsPerFile = 0;
|
||||
```
|
||||
|
||||
- [ ] **Schritt 2: `StartNewProject()` – Standardwerte setzen**
|
||||
|
||||
Nach `EditIsActive = true;` einfügen:
|
||||
|
||||
```csharp
|
||||
EditBackupEnabled = true;
|
||||
EditBackupUseCustomPath = false;
|
||||
EditBackupCustomPath = string.Empty;
|
||||
EditMaxBackupsPerFile = 0;
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: `EditProject()` – Werte aus ProjectConfig laden**
|
||||
|
||||
Nach `EditIsActive = project.IsActive;` einfügen:
|
||||
|
||||
```csharp
|
||||
EditBackupEnabled = project.BackupEnabled;
|
||||
EditBackupUseCustomPath = project.BackupPath is not null;
|
||||
EditBackupCustomPath = project.BackupPath ?? string.Empty;
|
||||
EditMaxBackupsPerFile = project.MaxBackupsPerFile;
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: `SaveAsync()` – Backup-Felder in DTOs mitsenden**
|
||||
|
||||
In `SaveAsync()`, die beiden DTO-Konstruktoraufrufe ersetzen:
|
||||
|
||||
```csharp
|
||||
// CreateProjectDto:
|
||||
await api.CreateProjectAsync(new CreateProjectDto(
|
||||
EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive,
|
||||
EditBackupEnabled,
|
||||
EditBackupUseCustomPath ? EditBackupCustomPath : null,
|
||||
EditMaxBackupsPerFile));
|
||||
|
||||
// UpdateProjectDto:
|
||||
await api.UpdateProjectAsync(SelectedProject!.Id, new UpdateProjectDto(
|
||||
EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive,
|
||||
EditBackupEnabled,
|
||||
EditBackupUseCustomPath ? EditBackupCustomPath : null,
|
||||
EditMaxBackupsPerFile));
|
||||
```
|
||||
|
||||
- [ ] **Schritt 5: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
|
||||
```
|
||||
|
||||
- [ ] **Schritt 6: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs
|
||||
git commit -m "feat(trayapp): Backup-Properties in ProjectManagementViewModel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 11: TrayApp – ProjectManagementWindow XAML + Code-Behind
|
||||
|
||||
**Files:**
|
||||
- Modify: `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml`
|
||||
- Modify: `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs`
|
||||
|
||||
- [ ] **Schritt 1: BACKUP-Sektion in das Editierformular einfügen**
|
||||
|
||||
In `ProjectManagementWindow.xaml`, direkt **vor** der `StatusMessage`-TextBlock-Zeile (ca. Zeile 116) folgende Sektion einfügen:
|
||||
|
||||
```xml
|
||||
<!-- BACKUP-Sektion -->
|
||||
<Separator Margin="0,8,0,12"/>
|
||||
<TextBlock Text="BACKUP" FontSize="10" FontWeight="Bold" Foreground="#888888"
|
||||
LetterSpacing="1" 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>
|
||||
```
|
||||
|
||||
**Hinweis:** Das `ProjectManagementWindow` hat bereits `BooleanToVisibilityConverter` als `BoolToVisConverter`. Für die inverse Sichtbarkeit der RadioButton-Binding muss ein `InverseBoolToVisibilityConverter` in den Window-Ressourcen ergänzt werden – **oder** es wird ein Code-Behind `IsChecked`-Binding auf `!EditBackupUseCustomPath` verwendet (nicht direkt in XAML). Einfachste Lösung: eine neue Converter-Klasse in `TrayApp/Converters/Converters.cs` und in `Window.Resources` registrieren (siehe Schritt 2).
|
||||
|
||||
- [ ] **Schritt 2: InverseBoolToVisibilityConverter in TrayApp hinzufügen**
|
||||
|
||||
In `EngineeringSync.TrayApp/Converters/Converters.cs` (bestehende Datei) einen neuen Converter hinzufügen:
|
||||
|
||||
```csharp
|
||||
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();
|
||||
}
|
||||
```
|
||||
|
||||
Und in `ProjectManagementWindow.xaml`, in `<Window.Resources>` registrieren:
|
||||
|
||||
```xml
|
||||
<conv:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisConverter"/>
|
||||
```
|
||||
|
||||
**Alternative:** Den RadioButton für „Gleicher Ordner" ohne Converter binden: einfach keine Binding – der zweite RadioButton (`EditBackupUseCustomPath = true`) und der erste sind in einer Gruppe; mit `GroupName` explizit koppeln und beide direkt binden:
|
||||
|
||||
```xml
|
||||
<RadioButton GroupName="BackupLocation" Content="Gleicher Ordner wie Simulationsdatei"
|
||||
IsChecked="{Binding EditBackupUseCustomPath,
|
||||
Converter={StaticResource InverseBoolToVisConverter}, Mode=TwoWay}"/>
|
||||
<RadioButton GroupName="BackupLocation" Content="Eigener Backup-Ordner"
|
||||
IsChecked="{Binding EditBackupUseCustomPath, Mode=TwoWay}"/>
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: Browse-Handler in Code-Behind hinzufügen**
|
||||
|
||||
In `ProjectManagementWindow.xaml.cs` (bestehende Datei), nach den vorhandenen Browse-Handlern einfügen:
|
||||
|
||||
```csharp
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: Build prüfen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
|
||||
```
|
||||
Häufige Fehler: fehlender `using System.Globalization;` in Converters.cs, fehlende Namespaces in XAML.
|
||||
|
||||
- [ ] **Schritt 5: Commit**
|
||||
|
||||
```bash
|
||||
git add EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml
|
||||
git add EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs
|
||||
git add EngineeringSync.TrayApp/Converters/Converters.cs
|
||||
git commit -m "feat(trayapp): BACKUP-Sektion in ProjectManagementWindow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Gesamtbuild und manuelle Verifikation
|
||||
|
||||
- [ ] **Schritt 1: Gesamtlösung bauen**
|
||||
|
||||
```bash
|
||||
dotnet build EngineeringSync.slnx
|
||||
```
|
||||
Erwartung: `Build succeeded` für alle 5 Projekte (Domain, Infrastructure, Service, TrayApp, Setup).
|
||||
|
||||
- [ ] **Schritt 2: Service starten und Migration prüfen**
|
||||
|
||||
```bash
|
||||
dotnet run --project EngineeringSync.Service
|
||||
```
|
||||
Erwartung: Service startet auf `http://localhost:5050`. In der Konsole erscheint die Migration-Meldung. Die SQLite-DB unter `%ProgramData%\EngineeringSync\engineeringsync.db` hat nun die Spalten `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile` in der `Projects`-Tabelle.
|
||||
|
||||
Prüfen mit:
|
||||
```bash
|
||||
# SQLite-Datei öffnen (DB Browser for SQLite oder sqlite3 CLI)
|
||||
# SELECT * FROM Projects LIMIT 1;
|
||||
# Alle 3 neuen Spalten müssen vorhanden sein.
|
||||
```
|
||||
|
||||
- [ ] **Schritt 3: API-Endpunkt testen**
|
||||
|
||||
```bash
|
||||
# Projekt erstellen mit Backup-Feldern
|
||||
curl -X POST http://localhost:5050/api/projects \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"Test\",\"engineeringPath\":\"C:\\\\Temp\\\\eng\",\"simulationPath\":\"C:\\\\Temp\\\\sim\",\"fileExtensions\":\".jt\",\"backupEnabled\":true,\"backupPath\":null,\"maxBackupsPerFile\":3}"
|
||||
|
||||
# Antwort muss Id, BackupEnabled=true, MaxBackupsPerFile=3 enthalten
|
||||
```
|
||||
|
||||
- [ ] **Schritt 4: TrayApp starten und Backup-Sektion prüfen**
|
||||
|
||||
```bash
|
||||
dotnet run --project EngineeringSync.TrayApp
|
||||
```
|
||||
- Projektverwaltung öffnen → Projekt bearbeiten → BACKUP-Sektion erscheint
|
||||
- Toggle deaktivieren → Optionen verschwinden
|
||||
- „Eigener Ordner" anklicken → Pfad-Eingabe und Browse erscheinen
|
||||
- Speichern → API-Aufruf mit Backup-Feldern
|
||||
|
||||
- [ ] **Schritt 5: Setup-Wizard starten und Seite prüfen**
|
||||
|
||||
```bash
|
||||
dotnet run --project EngineeringSync.Setup
|
||||
```
|
||||
- Durch alle Schritte navigieren → Schritt 4 „Backup" erscheint
|
||||
- Backup deaktivieren → Optionen ausblenden
|
||||
- Aktivieren + eigenen Ordner wählen + Max auf 5 setzen
|
||||
- Zusammenfassung zeigt Backup-Karte korrekt
|
||||
|
||||
- [ ] **Schritt 6: Final Commit**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: Backup-Einstellungen pro Projekt – vollständige Implementierung"
|
||||
```
|
||||
263
docs/superpowers/specs/2026-03-26-backup-settings-design.md
Normal file
263
docs/superpowers/specs/2026-03-26-backup-settings-design.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Spec: Pro-Projekt Backup-Einstellungen
|
||||
|
||||
**Datum:** 2026-03-26
|
||||
**Status:** Draft v3
|
||||
**Scope:** EngineeringSync – Backup-Konfiguration pro Projekt im Setup-Wizard und TrayApp
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Beim Sync erstellt der `SyncManager` aktuell immer eine `.bak`-Datei im gleichen Ordner wie die Zieldatei, ohne dass der User Kontrolle darüber hat. Ziel ist es, dem User pro Projekt volle Kontrolle über das Backup-Verhalten zu geben: aktivieren/deaktivieren, eigenen Backup-Ordner wählen, und eine Aufbewahrungsregel (max. N Backups pro Datei) festlegen.
|
||||
|
||||
---
|
||||
|
||||
## Anforderungen
|
||||
|
||||
### Funktional
|
||||
- Der User kann pro Projekt Backups **aktivieren oder deaktivieren**.
|
||||
- Bei aktivierten Backups kann der User wählen zwischen:
|
||||
- **Gleicher Ordner** wie die Zieldatei (bisheriges Verhalten)
|
||||
- **Eigener Backup-Ordner** (frei wählbarer Pfad)
|
||||
- Der User kann eine **Aufbewahrungsregel** festlegen:
|
||||
- `0` = unbegrenzt (alle Backups behalten)
|
||||
- `N > 0` = maximal N Backups pro Datei; älteste werden automatisch gelöscht
|
||||
- Die Einstellungen sind konfigurierbar:
|
||||
- Im **Setup-Wizard** (neue Seite zwischen „Erstes Projekt" und „Optionen")
|
||||
- Im **TrayApp `ProjectManagementWindow`** (neue BACKUP-Sektion beim Bearbeiten)
|
||||
|
||||
### Nicht-funktional
|
||||
- Standardwerte: `BackupEnabled = true`, `BackupPath = null` (gleicher Ordner), `MaxBackupsPerFile = 0`
|
||||
- Rückwärtskompatibilität: bestehende Projekte erhalten die Standardwerte per DB-Migration
|
||||
- Backup-Dateien folgen weiterhin dem Schema: `{name}_{yyyyMMdd_HHmmss}{ext}.bak`
|
||||
- Der Timestamp im Backup-Dateinamen verwendet `DateTime.Now` (Ortszeit), um für den User lesbar zu sein. Dies ist eine bewusste Abweichung von der UTC-Konvention im Rest der Anwendung.
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
### Ausgewählter Ansatz: Backup-Felder direkt in `ProjectConfig`
|
||||
|
||||
Drei neue Felder in der bestehenden `ProjectConfig`-Entity – keine neue Tabelle. Konsistent mit dem Muster des Projekts (alle Projekteinstellungen zentral in einer Entity).
|
||||
|
||||
**Verworfene Alternativen:**
|
||||
- Separate `ProjectBackupSettings`-Entity (1:1): Mehr Boilerplate für nur 3 Felder
|
||||
- JSON-Column: Verlust von EF-Typsicherheit, Overkill für 3 Felder
|
||||
|
||||
---
|
||||
|
||||
## Komponenten & Änderungen
|
||||
|
||||
### 1. Domain – `ProjectConfig`
|
||||
|
||||
```csharp
|
||||
public bool BackupEnabled { get; set; } = true;
|
||||
public string? BackupPath { get; set; } = null; // null = gleicher Ordner wie Zieldatei
|
||||
public int MaxBackupsPerFile { get; set; } = 0; // 0 = unbegrenzt
|
||||
```
|
||||
|
||||
### 2. Infrastructure – EF Migration
|
||||
|
||||
- Migration: `AddBackupSettingsToProjectConfig`
|
||||
- Standardwerte per `HasDefaultValue()` in `OnModelCreating` für bestehende Rows:
|
||||
- `BackupEnabled`: `true`
|
||||
- `BackupPath`: `null`
|
||||
- `MaxBackupsPerFile`: `0`
|
||||
|
||||
### 3. Service – `SyncManager`
|
||||
|
||||
**`BackupFile()` wird von `static` zur Instanzmethode** und erhält `project` als zweiten Parameter: `BackupFile(string targetPath, ProjectConfig project)`.
|
||||
|
||||
**Ablauf in `BackupFile(targetPath, project)`:**
|
||||
1. `if (!project.BackupEnabled) return;` — kein Backup, sofort zurück
|
||||
2. `backupDir = project.BackupPath ?? Path.GetDirectoryName(targetPath)`
|
||||
3. `Directory.CreateDirectory(backupDir)` — **nur** Backup-Verzeichnis (unabhängig vom Ziel-Verzeichnis)
|
||||
4. Backup-Dateiname: `{name}_{DateTime.Now:yyyyMMdd_HHmmss}{ext}.bak`
|
||||
5. Backup-Datei verschieben (laufwerk-übergreifend sicher):
|
||||
- Versuch: `File.Move(targetPath, backupPath)` (schnell, gleiche Volume)
|
||||
- Falls `IOException` (z.B. andere Volume, Netzlaufwerk): `File.Copy` + `File.Delete` als Fallback
|
||||
6. `if (project.MaxBackupsPerFile > 0) CleanupOldBackups(backupDir, baseName, ext, project.MaxBackupsPerFile)`
|
||||
|
||||
**`ProcessChangeAsync(change, ct)` – beide Call-Sites werden aktualisiert:**
|
||||
|
||||
Die Methode hat zwei Stellen, an denen `BackupFile` aufgerufen wird:
|
||||
|
||||
```
|
||||
// Stelle 1: Deleted-Branch (Zeile ~57)
|
||||
if (File.Exists(targetPath))
|
||||
{
|
||||
BackupFile(targetPath, project); // ← project hinzufügen
|
||||
File.Delete(targetPath);
|
||||
}
|
||||
|
||||
// Stelle 2: Overwrite-Branch (Zeile ~69)
|
||||
if (File.Exists(targetPath))
|
||||
BackupFile(targetPath, project); // ← project hinzufügen
|
||||
```
|
||||
|
||||
**`Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!)` bleibt unverändert** (Zeile ~66 im aktuellen Code) – dies ist die Zielverzeichnis-Erstellung und ist von der Backup-Verzeichnis-Erstellung in `BackupFile()` unabhängig.
|
||||
|
||||
**Verhalten bei `ChangeType.Deleted`:**
|
||||
- `BackupEnabled = true`: Zieldatei wird **gesichert** (`.bak`), dann gelöscht. (Konsistent mit bisherigem Verhalten.)
|
||||
- `BackupEnabled = false`: Zieldatei wird **unwiederbringlich gelöscht**. Dies ist gewünschtes Verhalten – der User hat Backups explizit deaktiviert.
|
||||
|
||||
**Neue `CleanupOldBackups()`-Methode:**
|
||||
```
|
||||
CleanupOldBackups(backupDir, baseName, ext, max):
|
||||
1. Suche alle Dateien: {baseName}_*{ext}.bak im backupDir
|
||||
2. Sortiere alphabetisch aufsteigend nach Dateiname
|
||||
(Format yyyyMMdd_HHmmss ist lexikografisch geordnet → korrekte zeitliche Reihenfolge)
|
||||
NICHT nach Filesystem-Erstelldatum sortieren (File.Move behält das Original-Erstelldatum)
|
||||
3. Lösche alle Einträge über dem Limit (älteste zuerst = vorne in der sortierten Liste)
|
||||
4. Fehler beim Löschen: LogWarning, kein Abbruch
|
||||
```
|
||||
|
||||
**Rename-Szenario (`ChangeType.Renamed`):**
|
||||
Backups, die unter dem alten Dateinamen angelegt wurden, werden von `CleanupOldBackups` nicht erfasst (anderer `baseName`). Diese orphaned Backups akkumulieren sich unter dem alten Namen. Dieses Verhalten ist **explizit akzeptiert** und liegt außerhalb des Scopes dieser Änderung.
|
||||
|
||||
### 4. Service – API-Modelle
|
||||
|
||||
`CreateProjectRequest` und `UpdateProjectRequest` erhalten:
|
||||
```csharp
|
||||
bool BackupEnabled = true
|
||||
string? BackupPath = null
|
||||
int MaxBackupsPerFile = 0
|
||||
```
|
||||
|
||||
**Validierungsregel für `BackupPath` in `ProjectsApi`:**
|
||||
`BackupPath` wird bei POST/PUT **nicht** auf Existenz geprüft. Fehlende Verzeichnisse werden beim ersten Sync per `Directory.CreateDirectory()` automatisch erzeugt. (Abweichend von `EngineeringPath`/`SimulationPath`, die auf Existenz geprüft werden.)
|
||||
|
||||
`ProjectsApi` mappt die neuen Felder beim Erstellen/Aktualisieren der `ProjectConfig`.
|
||||
|
||||
### 5. Setup-Wizard
|
||||
|
||||
**Neue Datei:** `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml` + `.cs`
|
||||
|
||||
**`WizardState` – neue Properties:**
|
||||
```csharp
|
||||
[ObservableProperty] private bool _backupEnabled = true;
|
||||
[ObservableProperty] private bool _backupUseCustomPath = false;
|
||||
[ObservableProperty] private string _backupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _maxBackupsPerFile = 0;
|
||||
```
|
||||
|
||||
**`WizardViewModel` – Wizard-Schritte steigen von 7 auf 8:**
|
||||
```
|
||||
Index 0: Willkommen
|
||||
Index 1: Installation
|
||||
Index 2: Erstes Projekt
|
||||
Index 3: Backup ← NEU WizardStep("Backup", "\uE72E")
|
||||
Index 4: Optionen
|
||||
Index 5: Zusammenfassung
|
||||
Index 6: Installation
|
||||
Index 7: Fertig
|
||||
```
|
||||
|
||||
`WizardViewModel.Steps` erhält `WizardStep("Backup", "\uE72E")` an Position 3. `_pageFactories` erhält `() => new BackupOptionsPage(this)` an Position 3. `IsLastStep = Steps.Count - 2` bleibt unverändert (ergibt mit 8 Schritten korrekt Index 6 = „Zusammenfassung").
|
||||
|
||||
**`BackupOptionsPage`-Inhalt:**
|
||||
- `OptionCard` / `CheckBox`: „Backups vor dem Überschreiben aktivieren" (Standard: an)
|
||||
- Wenn aktiviert (Visibility via `BoolToVisibilityConverter`):
|
||||
- Radio: „Gleicher Ordner wie Simulationsdatei" *(Standard)*
|
||||
- Radio: „Eigener Backup-Ordner" → TextBox + Browse-Button (`FolderBrowserDialog`)
|
||||
- TextBox + Label: „Maximal ___ Backups pro Datei (0 = unbegrenzt)"
|
||||
|
||||
**`InstallerService.WriteFirstRunConfig()`** schreibt Backup-Felder als neuen `Backup`-Abschnitt in `firstrun-config.json`:
|
||||
```json
|
||||
{
|
||||
"FirstRun": { ... },
|
||||
"Backup": {
|
||||
"BackupEnabled": true,
|
||||
"BackupPath": null,
|
||||
"MaxBackupsPerFile": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`Program.cs` – `ProcessFirstRunConfigAsync()`-Funktion** (NICHT `Worker.cs` – die Verarbeitung der `firstrun-config.json` liegt als lokale Funktion in `Program.cs`, Zeile ~42):
|
||||
- Liest den neuen `Backup`-Abschnitt via `JsonDocument` (mit `TryGetProperty` für Rückwärtskompatibilität, falls `Backup`-Abschnitt fehlt → Standardwerte)
|
||||
- Setzt `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile` beim Erstellen der `ProjectConfig`
|
||||
|
||||
**`SummaryPage`** zeigt:
|
||||
- `SummaryBoolRow`: „Backups aktiviert"
|
||||
- `SummaryRow` (conditional, wenn `BackupUseCustomPath`): „Backup-Ordner: {Pfad}"
|
||||
- `SummaryRow` (conditional, wenn `MaxBackupsPerFile > 0`): „Max. Backups pro Datei: {N}"
|
||||
|
||||
### 6. TrayApp – `ProjectManagementWindow`
|
||||
|
||||
**`ProjectManagementViewModel` – neue Properties:**
|
||||
```csharp
|
||||
[ObservableProperty] private bool _backupEnabled;
|
||||
[ObservableProperty] private bool _backupUseCustomPath;
|
||||
[ObservableProperty] private string _backupCustomPath = string.Empty;
|
||||
[ObservableProperty] private int _maxBackupsPerFile;
|
||||
```
|
||||
|
||||
**XAML – neue BACKUP-Sektion** im Bearbeitungs-Formular (analog zu bestehenden Sektionen):
|
||||
```
|
||||
BACKUP
|
||||
├── CheckBox: „Backups vor dem Überschreiben erstellen"
|
||||
├── (sichtbar wenn aktiviert, via BoolToVisibilityConverter):
|
||||
│ ├── RadioButton: „Gleicher Ordner wie Simulationsdatei"
|
||||
│ ├── RadioButton: „Eigener Ordner:" + TextBox + [Durchsuchen]
|
||||
│ └── TextBox: „Maximal ___ Backups pro Datei (0 = unbegrenzt)"
|
||||
```
|
||||
|
||||
**TrayApp `ApiClient` – DTOs:**
|
||||
`CreateProjectDto` und `UpdateProjectDto` in `ApiClient.cs` erhalten:
|
||||
```csharp
|
||||
bool BackupEnabled = true
|
||||
string? BackupPath = null
|
||||
int MaxBackupsPerFile = 0
|
||||
```
|
||||
|
||||
`ApiClient.CreateProjectAsync()` und `UpdateProjectAsync()` senden die Backup-Felder mit.
|
||||
|
||||
---
|
||||
|
||||
## Datenfluss
|
||||
|
||||
```
|
||||
[Wizard BackupOptionsPage]
|
||||
↓ WizardState
|
||||
[InstallerService] → firstrun-config.json (Backup-Abschnitt)
|
||||
↓
|
||||
[Program.cs ProcessFirstRunConfigAsync()] → ProjectConfig (mit Backup-Feldern) in SQLite
|
||||
|
||||
[TrayApp ProjectManagementWindow]
|
||||
↓ PUT /api/projects/{id} (mit Backup-Feldern in UpdateProjectDto)
|
||||
[ProjectsApi] → ProjectConfig in SQLite
|
||||
↓
|
||||
[SyncManager.ProcessChangeAsync]
|
||||
├─ Directory.CreateDirectory(targetDir) ← Zielverzeichnis
|
||||
└─ BackupFile(targetPath, project)
|
||||
├─ if !BackupEnabled → return
|
||||
├─ backupDir = BackupPath ?? targetDir
|
||||
├─ Directory.CreateDirectory(backupDir) ← Backup-Verzeichnis
|
||||
├─ File.Move(targetPath, backupPath)
|
||||
└─ CleanupOldBackups() wenn MaxBackupsPerFile > 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| Szenario | Verhalten |
|
||||
|---|---|
|
||||
| Backup-Ordner existiert nicht | `Directory.CreateDirectory()` in `BackupFile()` erzeugt ihn beim ersten Sync |
|
||||
| Backup-Ordner nicht beschreibbar | Exception → von `ProcessChangeAsync` gefangen → `failed`-Zähler |
|
||||
| Cleanup schlägt fehl | `logger.LogWarning`, kein Sync-Abbruch |
|
||||
| `BackupEnabled=false` + `Deleted` | Zieldatei wird unwiederbringlich gelöscht (gewünscht) |
|
||||
| `BackupEnabled=true` + `Deleted` | Backup wird erstellt, dann Zieldatei gelöscht (bisheriges Verhalten) |
|
||||
| `BackupPath` bei POST/PUT nicht existent | Kein API-Fehler; Verzeichnis wird beim ersten Sync erstellt |
|
||||
| Rename-Szenario: alte Backups | Akkumulieren unter altem Dateinamen; kein automatisches Cleanup (out of scope) |
|
||||
|
||||
---
|
||||
|
||||
## Nicht im Scope
|
||||
|
||||
- Backups wiederherstellen (Restore-UI)
|
||||
- Backup-Komprimierung (.zip)
|
||||
- Backup-Statistiken / Speicherplatz-Anzeige
|
||||
- Globale (projektübergreifende) Backup-Einstellungen
|
||||
- Cleanup von Backups unter altem Dateinamen nach Rename
|
||||
82
installer/build-installer.ps1
Normal file
82
installer/build-installer.ps1
Normal file
@@ -0,0 +1,82 @@
|
||||
# ======================================================================
|
||||
# build-installer.ps1 - Publiziert alle Projekte und erstellt den Installer
|
||||
# Aufruf: .\installer\build-installer.ps1
|
||||
# ======================================================================
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = Split-Path $PSScriptRoot -Parent
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "+----------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host "| EngineeringSync Installer Build |" -ForegroundColor Cyan
|
||||
Write-Host "+----------------------------------------------+" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# -- 1. Service publizieren (self-contained) -------------------------------------------
|
||||
Write-Host "[1/4] Publiziere Windows Service (self-contained)..." -ForegroundColor Yellow
|
||||
dotnet publish "$root\EngineeringSync.Service\EngineeringSync.Service.csproj" `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
-o "$root\EngineeringSync.Service\bin\Release\net10.0\publish"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "Service-Publish fehlgeschlagen" }
|
||||
Write-Host " [OK] Service publiziert (self-contained)" -ForegroundColor Green
|
||||
|
||||
# -- 2. TrayApp publizieren (self-contained) -------------------------------------------
|
||||
Write-Host "[2/4] Publiziere TrayApp (self-contained)..." -ForegroundColor Yellow
|
||||
dotnet publish "$root\EngineeringSync.TrayApp\EngineeringSync.TrayApp.csproj" `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
-o "$root\EngineeringSync.TrayApp\bin\Release\net10.0-windows\publish"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "TrayApp-Publish fehlgeschlagen" }
|
||||
Write-Host " [OK] TrayApp publiziert (self-contained)" -ForegroundColor Green
|
||||
|
||||
# -- 3. Setup-Wizard publizieren (self-contained) --------------------------------------
|
||||
Write-Host "[3/4] Publiziere Setup-Wizard..." -ForegroundColor Yellow
|
||||
dotnet publish "$root\EngineeringSync.Setup\EngineeringSync.Setup.csproj" `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
--output "$root\EngineeringSync.Setup\bin\Release\net10.0-windows\publish"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "Setup-Wizard-Publish fehlgeschlagen" }
|
||||
Write-Host " [OK] Setup-Wizard publiziert (self-contained)" -ForegroundColor Green
|
||||
|
||||
# -- 4. Inno Setup ausfuehren -----------------------------------------
|
||||
Write-Host "[4/4] Erstelle Installer mit Inno Setup..." -ForegroundColor Yellow
|
||||
|
||||
$isccCmd = Get-Command "iscc" -ErrorAction SilentlyContinue
|
||||
$isccPaths = @(
|
||||
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
|
||||
"C:\Program Files\Inno Setup 6\ISCC.exe",
|
||||
$(if ($isccCmd) { $isccCmd.Source } else { $null })
|
||||
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
|
||||
|
||||
if (-not $isccPaths) {
|
||||
Write-Host ""
|
||||
Write-Host " WARNUNG: Inno Setup nicht gefunden!" -ForegroundColor Red
|
||||
Write-Host " Bitte installieren: https://jrsoftware.org/isinfo.php" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host " Alternativ manuell kompilieren:" -ForegroundColor Yellow
|
||||
Write-Host " ISCC.exe ""$PSScriptRoot\setup.iss""" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "$root\dist" | Out-Null
|
||||
& $isccPaths "$PSScriptRoot\setup.iss"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "Inno Setup fehlgeschlagen" }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "+----------------------------------------------+" -ForegroundColor Green
|
||||
Write-Host "| [OK] Installer erfolgreich erstellt! |" -ForegroundColor Green
|
||||
Write-Host "+----------------------------------------------+" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Ausgabe: $root\dist" -ForegroundColor Cyan
|
||||
|
||||
# Installer im Explorer oeffnen
|
||||
$distPath = "$root\dist"
|
||||
Start-Process "explorer.exe" $distPath
|
||||
190
installer/setup.iss
Normal file
190
installer/setup.iss
Normal file
@@ -0,0 +1,190 @@
|
||||
; ======================================================================
|
||||
; EngineeringSync - Inno Setup Script
|
||||
; Erstellt einen professionellen Windows-Installer (.exe)
|
||||
;
|
||||
; Voraussetzung: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
||||
; Build: .\installer\build-installer.ps1
|
||||
; ======================================================================
|
||||
|
||||
#define AppName "EngineeringSync"
|
||||
#define AppVersion "1.0.0"
|
||||
#define AppPublisher "EngineeringSync"
|
||||
#define AppExe "EngineeringSync.TrayApp.exe"
|
||||
#define ServiceExe "EngineeringSync.Service.exe"
|
||||
#define SetupExe "EngineeringSync.Setup.exe"
|
||||
#define DefaultDir "{autopf}\EngineeringSync"
|
||||
#define TrayIcon "..\EngineeringSync.TrayApp\Assets\tray.ico"
|
||||
|
||||
; -- Pfade zu den publizierten Builds (relative zum .iss-Verzeichnis) --
|
||||
#define ServicePublish "..\EngineeringSync.Service\bin\Release\net10.0\publish\"
|
||||
#define TrayPublish "..\EngineeringSync.TrayApp\bin\Release\net10.0-windows\publish\"
|
||||
#define SetupPublish "..\EngineeringSync.Setup\bin\Release\net10.0-windows\publish\"
|
||||
; Self-contained: .NET Runtime wird nicht mehr separat benoetigt
|
||||
|
||||
|
||||
[Setup]
|
||||
AppId = {{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
|
||||
AppName = {#AppName}
|
||||
AppVersion = {#AppVersion}
|
||||
AppPublisher = {#AppPublisher}
|
||||
AppPublisherURL = https://github.com/your-org/EngineeringSync
|
||||
AppSupportURL = https://github.com/your-org/EngineeringSync/issues
|
||||
DefaultDirName = {#DefaultDir}
|
||||
DefaultGroupName = {#AppName}
|
||||
DisableProgramGroupPage = yes
|
||||
PrivilegesRequired = admin
|
||||
OutputBaseFilename = EngineeringSync-{#AppVersion}-Setup
|
||||
OutputDir = ..\dist
|
||||
Compression = lzma2/ultra64
|
||||
SolidCompression = yes
|
||||
ArchitecturesAllowed = x64compatible
|
||||
ArchitecturesInstallIn64BitMode = x64compatible
|
||||
|
||||
; Installer-Icon (Titelleiste + Explorer)
|
||||
SetupIconFile = {#TrayIcon}
|
||||
|
||||
; Wizard-Design
|
||||
WizardStyle = modern
|
||||
WizardResizable = no
|
||||
WindowVisible = no
|
||||
|
||||
; Versionsinformationen
|
||||
VersionInfoVersion = {#AppVersion}
|
||||
VersionInfoCompany = {#AppPublisher}
|
||||
VersionInfoDescription = {#AppName} Setup
|
||||
VersionInfoProductName = {#AppName}
|
||||
|
||||
; Deinstallation
|
||||
UninstallDisplayName = {#AppName}
|
||||
UninstallDisplayIcon = {app}\{#AppExe}
|
||||
CreateUninstallRegKey = yes
|
||||
|
||||
|
||||
[Languages]
|
||||
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
|
||||
|
||||
|
||||
[Messages]
|
||||
WelcomeLabel1=Willkommen beim [name]-Setup-Assistenten
|
||||
WelcomeLabel2=Dieser Assistent installiert [name/ver] auf Ihrem Computer.%n%nBitte schließen Sie alle anderen Anwendungen bevor Sie fortfahren.
|
||||
FinishedLabel=Die Installation von [name] ist abgeschlossen.%n%nDer Setup-Assistent wird jetzt geöffnet, um Ihre erste Konfiguration einzurichten.
|
||||
ClickFinish=Klicken Sie auf Fertigstellen um den EngineeringSync-Konfigurations-Assistenten zu starten.
|
||||
|
||||
|
||||
[Types]
|
||||
Name: "full"; Description: "Vollständige Installation"
|
||||
Name: "custom"; Description: "Benutzerdefinierte Installation"; Flags: iscustom
|
||||
|
||||
|
||||
[Components]
|
||||
Name: "service"; Description: "EngineeringSync Windows-Dienst (erforderlich)"; Types: full custom; Flags: fixed
|
||||
Name: "trayapp"; Description: "System Tray Benachrichtigungs-App"; Types: full custom; Flags: fixed
|
||||
Name: "setup"; Description: "Konfigurations-Assistent"; Types: full custom; Flags: fixed
|
||||
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "Desktop-Verknüpfung erstellen"; GroupDescription: "Zusätzliche Symbole:"
|
||||
|
||||
|
||||
[Files]
|
||||
; -- Windows Service --
|
||||
Source: "{#ServicePublish}*"; DestDir: "{app}"; Components: service; Flags: ignoreversion recursesubdirs
|
||||
|
||||
; -- TrayApp --
|
||||
Source: "{#TrayPublish}*"; DestDir: "{app}"; Components: trayapp; Flags: ignoreversion recursesubdirs
|
||||
|
||||
; -- Setup-Wizard --
|
||||
Source: "{#SetupPublish}*"; DestDir: "{app}"; Components: setup; Flags: ignoreversion recursesubdirs
|
||||
|
||||
; -- Icons als eigenständige Dateien (für Verknüpfungen) --
|
||||
Source: "..\EngineeringSync.TrayApp\Assets\tray.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\EngineeringSync.Setup\Assets\setup-icon.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
|
||||
|
||||
[Icons]
|
||||
; Startmenü - TrayApp
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExe}"; IconFilename: "{app}\tray.ico"; Comment: "EngineeringSync Benachrichtigungen"
|
||||
|
||||
; Startmenü - Konfiguration
|
||||
Name: "{group}\{#AppName} konfigurieren"; Filename: "{app}\{#SetupExe}"; IconFilename: "{app}\setup-icon.ico"; Comment: "EngineeringSync konfigurieren"
|
||||
|
||||
; Startmenü - Deinstallieren
|
||||
Name: "{group}\{#AppName} deinstallieren"; Filename: "{uninstallexe}"; IconFilename: "{app}\tray.ico"
|
||||
|
||||
; Desktop-Verknüpfung (nur wenn Task gewählt)
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExe}"; IconFilename: "{app}\tray.ico"; Comment: "EngineeringSync Benachrichtigungen"; Tasks: desktopicon
|
||||
|
||||
|
||||
[Run]
|
||||
; Windows-Dienst registrieren
|
||||
Filename: "{sys}\sc.exe"; \
|
||||
Parameters: "create EngineeringSync binPath= ""{app}\{#ServiceExe}"" start= auto DisplayName= ""EngineeringSync Watcher Service"""; \
|
||||
StatusMsg: "Windows-Dienst wird registriert..."; \
|
||||
Flags: runhidden waituntilterminated; \
|
||||
Check: not ServiceExists
|
||||
|
||||
; Konfigurations-Wizard starten (nach der Installation)
|
||||
Filename: "{app}\{#SetupExe}"; \
|
||||
Description: "EngineeringSync jetzt konfigurieren"; \
|
||||
StatusMsg: "Konfigurations-Assistent wird gestartet..."; \
|
||||
Flags: postinstall nowait skipifsilent
|
||||
|
||||
|
||||
[UninstallRun]
|
||||
; Dienst stoppen
|
||||
Filename: "{sys}\sc.exe"; Parameters: "stop EngineeringSync"; \
|
||||
Flags: runhidden waituntilterminated; RunOnceId: "StopService"
|
||||
|
||||
; Dienst löschen
|
||||
Filename: "{sys}\sc.exe"; Parameters: "delete EngineeringSync"; \
|
||||
Flags: runhidden waituntilterminated; RunOnceId: "DeleteService"
|
||||
|
||||
; TrayApp beenden
|
||||
Filename: "{sys}\taskkill.exe"; Parameters: "/F /IM ""{#AppExe}"""; \
|
||||
Flags: runhidden waituntilterminated; RunOnceId: "KillTray"
|
||||
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{commonappdata}\EngineeringSync"
|
||||
|
||||
|
||||
[Registry]
|
||||
; TrayApp-Autostart entfernen bei Deinstallation
|
||||
Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; \
|
||||
ValueType: none; ValueName: "EngineeringSync.TrayApp"; Flags: deletevalue uninsdeletevalue
|
||||
|
||||
|
||||
[Code]
|
||||
{ ======================================================================
|
||||
Pascal-Script
|
||||
====================================================================== }
|
||||
|
||||
function ServiceExists(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Exec(ExpandConstant('{sys}\sc.exe'), 'query EngineeringSync',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Result := (ResultCode = 0);
|
||||
end;
|
||||
|
||||
{ Upgrade: Bestehende Installation erkennen und stoppen }
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
|
||||
if ServiceExists() then
|
||||
begin
|
||||
Exec(ExpandConstant('{sys}\sc.exe'), 'stop EngineeringSync',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Sleep(2000);
|
||||
end;
|
||||
|
||||
{ TrayApp beenden falls läuft }
|
||||
Exec(ExpandConstant('{sys}\taskkill.exe'),
|
||||
'/F /IM "EngineeringSync.TrayApp.exe"',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
210
plan.md
Normal file
210
plan.md
Normal file
@@ -0,0 +1,210 @@
|
||||
# Project Context: Engineering-to-Simulation Sync Tool ("EngineeringSync")
|
||||
|
||||
You are an expert C# / .NET 10 developer. Build a middleware tool that bridges mechanical engineering (CAD) and simulation (Process Simulate).
|
||||
|
||||
**Problem:** Engineers save files to a shared folder. Immediate overwrites in the simulation folder cause data corruption for the simulator. This tool watches the engineering folder, records changes, and notifies the simulation engineer via a WPF System Tray App. The simulation engineer reviews changes, selects what to sync, and triggers a controlled copy.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** .NET 10
|
||||
- **Service:** Worker Service (Windows Service) + ASP.NET Core Minimal API + SignalR
|
||||
- **Client:** WPF (.NET 10-Windows) with `H.NotifyIcon.Wpf`
|
||||
- **Database:** SQLite via EF Core 10 (WAL mode for concurrency)
|
||||
- **File Watching:** `System.IO.FileSystemWatcher` + `System.Threading.Channels` (debouncing)
|
||||
- **UI Pattern:** MVVM with `CommunityToolkit.Mvvm`
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Solution Initialization
|
||||
|
||||
Create solution structure via .NET CLI:
|
||||
|
||||
```
|
||||
EngineeringSync.sln
|
||||
├── EngineeringSync.Domain (Class Library, net10.0) – Entities, Enums, Interfaces
|
||||
├── EngineeringSync.Infrastructure (Class Library, net10.0) – EF Core, AppDbContext, Migrations
|
||||
├── EngineeringSync.Service (Worker Service, net10.0) – Kestrel :5050, SignalR, Watcher
|
||||
└── EngineeringSync.TrayApp (WPF App, net10.0-windows) – System tray + all UI windows
|
||||
```
|
||||
|
||||
**Dependency direction (strict one-way):**
|
||||
- `Infrastructure` → `Domain`
|
||||
- `Service` → `Domain` + `Infrastructure`
|
||||
- `TrayApp` → `Domain` (for shared DTOs/Enums only)
|
||||
|
||||
**NuGet Packages:**
|
||||
|
||||
| Project | Packages |
|
||||
|---|---|
|
||||
| Domain | *(none – pure POCOs)* |
|
||||
| Infrastructure | `Microsoft.EntityFrameworkCore.Sqlite`, `Microsoft.EntityFrameworkCore.Design` |
|
||||
| Service | `Microsoft.Extensions.Hosting.WindowsServices`, `Microsoft.AspNetCore.SignalR` |
|
||||
| TrayApp | `H.NotifyIcon.Wpf`, `Microsoft.AspNetCore.SignalR.Client`, `CommunityToolkit.Mvvm` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Domain & Database Modeling
|
||||
|
||||
### Domain Entities (`EngineeringSync.Domain`)
|
||||
|
||||
**`ProjectConfig`** – A watched project (persisted in DB, managed via UI):
|
||||
- `Id` (Guid, PK)
|
||||
- `Name` (string) – Display name, e.g. "Hauptprojekt Karosserie"
|
||||
- `EngineeringPath` (string) – Source folder the watcher monitors
|
||||
- `SimulationPath` (string) – Target folder for controlled sync
|
||||
- `FileExtensions` (string) – Comma-separated, e.g. ".jt,.cojt,.xml"
|
||||
- `IsActive` (bool) – Enable/disable watching without deleting
|
||||
- `CreatedAt` (DateTime)
|
||||
|
||||
**`FileRevision`** – Tracks known file state for change detection:
|
||||
- `Id` (Guid, PK)
|
||||
- `ProjectId` (Guid, FK → ProjectConfig)
|
||||
- `RelativePath` (string)
|
||||
- `FileHash` (string) – SHA-256
|
||||
- `Size` (long)
|
||||
- `LastModified` (DateTime)
|
||||
|
||||
**`PendingChange`** – A detected change awaiting user action:
|
||||
- `Id` (Guid, PK)
|
||||
- `ProjectId` (Guid, FK → ProjectConfig)
|
||||
- `RelativePath` (string)
|
||||
- `ChangeType` (Enum: Created, Modified, Renamed, Deleted)
|
||||
- `OldRelativePath` (string?) – Only for Renamed
|
||||
- `Status` (Enum: Pending, Synced, Ignored)
|
||||
- `CreatedAt` (DateTime)
|
||||
- `SyncedAt` (DateTime?)
|
||||
|
||||
### Infrastructure (`EngineeringSync.Infrastructure`)
|
||||
|
||||
- `AppDbContext` with `DbSet<ProjectConfig>`, `DbSet<FileRevision>`, `DbSet<PendingChange>`
|
||||
- SQLite connection string with `Mode=ReadWriteCreate` and WAL pragma
|
||||
- Auto-migration at service startup via `context.Database.Migrate()`
|
||||
- Unique index on `(ProjectId, RelativePath)` for `FileRevision`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: The Windows Service (Core Logic)
|
||||
|
||||
### 3.1 Project Management API (CRUD)
|
||||
|
||||
New endpoints for managing projects from the TrayApp UI:
|
||||
|
||||
- `GET /api/projects` – List all ProjectConfig
|
||||
- `POST /api/projects` – Create new ProjectConfig (validates paths exist)
|
||||
- `PUT /api/projects/{id}` – Update ProjectConfig (restarts watcher for that project)
|
||||
- `DELETE /api/projects/{id}` – Delete ProjectConfig + associated changes
|
||||
|
||||
When a ProjectConfig is created or updated with `IsActive = true`, the WatcherService must dynamically start/stop the corresponding `FileSystemWatcher`.
|
||||
|
||||
### 3.2 FileSystemWatcher & Debouncing (`WatcherService`)
|
||||
|
||||
- `BackgroundService` that manages one `FileSystemWatcher` per active `ProjectConfig`
|
||||
- Listen to: `Created`, `Changed`, `Renamed`, `Deleted`
|
||||
- Filter by `ProjectConfig.FileExtensions`
|
||||
- **Debouncing pipeline:**
|
||||
1. Push raw `FileSystemEventArgs` into a `Channel<FileEvent>` (unbounded)
|
||||
2. Consumer reads from channel, groups events by `(ProjectId, RelativePath)` within a 2000ms sliding window
|
||||
3. After window closes, compute SHA-256 hash of file
|
||||
4. Compare hash against latest `FileRevision` for that path
|
||||
5. If hash differs (or file is new): write `FileRevision` + `PendingChange` to DB
|
||||
6. Broadcast via SignalR
|
||||
- **Renamed:** Record old + new path, update `FileRevision.RelativePath`
|
||||
- **Deleted:** Record as `ChangeType.Deleted`, remove `FileRevision`
|
||||
|
||||
### 3.3 Minimal API & SignalR Hub
|
||||
|
||||
Host Kestrel on `http://localhost:5050`:
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/projects` | List all projects |
|
||||
| POST | `/api/projects` | Create project |
|
||||
| PUT | `/api/projects/{id}` | Update project |
|
||||
| DELETE | `/api/projects/{id}` | Delete project |
|
||||
| GET | `/api/changes/{projectId}` | Pending changes for project |
|
||||
| GET | `/api/changes/{projectId}/history` | Synced/ignored changes (last 100) |
|
||||
| POST | `/api/sync` | Sync selected PendingChange IDs |
|
||||
| POST | `/api/ignore` | Ignore selected PendingChange IDs |
|
||||
|
||||
**SignalR Hub** (`NotificationHub` at `/notifications`):
|
||||
- `ReceiveChangeNotification(projectId, projectName, count)` – Fired when new PendingChanges are persisted
|
||||
- `ProjectConfigChanged()` – Fired when a project is created/updated/deleted
|
||||
|
||||
### 3.4 Sync Logic (`SyncManager`)
|
||||
|
||||
When `POST /api/sync` is called with a list of PendingChange IDs:
|
||||
|
||||
1. Load PendingChange + ProjectConfig from DB
|
||||
2. Verify source file exists at `EngineeringPath / RelativePath`
|
||||
3. If target already exists in `SimulationPath`:
|
||||
- Rename to `{filename}_{timestamp:yyyyMMdd_HHmmss}.bak` (timestamped to prevent overwriting previous backups)
|
||||
4. Copy source → target (create subdirectories as needed)
|
||||
5. Mark PendingChange as `Status = Synced`, set `SyncedAt`
|
||||
6. For `ChangeType.Deleted`: Delete target file (after backup), mark Synced
|
||||
7. Return result summary (success count, errors)
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: The WPF Tray App
|
||||
|
||||
### 4.1 System Tray Setup
|
||||
|
||||
- `H.NotifyIcon.Wpf`: headless startup (no main window)
|
||||
- Tray icon context menu:
|
||||
- **"Änderungen anzeigen"** → Opens PendingChangesWindow
|
||||
- **"Projekte verwalten"** → Opens ProjectManagementWindow
|
||||
- **"Beenden"** → Graceful shutdown
|
||||
|
||||
### 4.2 SignalR Client
|
||||
|
||||
- Connect to `http://localhost:5050/notifications` on startup
|
||||
- Auto-reconnect with exponential backoff
|
||||
- On `ReceiveChangeNotification`: Show balloon tip "Neue Engineering-Daten für Projekt [Name] verfügbar. Klicken zum Überprüfen."
|
||||
- On `ProjectConfigChanged`: Refresh project list in any open window
|
||||
|
||||
### 4.3 Project Management Window (NEW)
|
||||
|
||||
A WPF Window (MVVM) for managing watched projects:
|
||||
|
||||
- **ListView** showing all ProjectConfig entries (Name, EngineeringPath, SimulationPath, Active-Status)
|
||||
- **"Neues Projekt"** button → Opens inline form or dialog:
|
||||
- Name (TextBox)
|
||||
- Engineering-Pfad (TextBox + FolderBrowserDialog via Button)
|
||||
- Simulations-Pfad (TextBox + FolderBrowserDialog via Button)
|
||||
- Dateiendungen (TextBox, comma-separated, default: ".jt,.cojt,.xml")
|
||||
- Aktiv (CheckBox)
|
||||
- **"Bearbeiten"** / **"Löschen"** buttons per row
|
||||
- All CRUD calls go to `POST/PUT/DELETE /api/projects`
|
||||
- Path validation: Check that folders exist before saving
|
||||
|
||||
### 4.4 Pending Changes Window
|
||||
|
||||
- MVVM with `DataGrid`
|
||||
- **Project selector** (ComboBox) at top → fetches from `GET /api/projects`
|
||||
- Columns: Datei, Änderungstyp, Zeitstempel, Auswahl (CheckBox)
|
||||
- **"Ausgewählte synchronisieren"** → `POST /api/sync`
|
||||
- **"Ausgewählte ignorieren"** → `POST /api/ignore`
|
||||
- **"Alle synchronisieren"** / **"Alle ignorieren"** convenience buttons
|
||||
- Auto-refresh when SignalR notification arrives for the selected project
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Build Order & Execution
|
||||
|
||||
Build step-by-step, verify each phase compiles before proceeding:
|
||||
|
||||
1. **Phase 1** – Solution scaffolding via `dotnet` CLI. Verify `dotnet build` succeeds.
|
||||
2. **Phase 2** – Domain entities + EF Core DbContext + initial migration. Verify migration applies.
|
||||
3. **Phase 3.1** – Project CRUD API endpoints. Test with manual HTTP requests.
|
||||
4. **Phase 3.2** – WatcherService with Channel debouncing. Test with file drops.
|
||||
5. **Phase 3.3** – SignalR hub + remaining API endpoints.
|
||||
6. **Phase 3.4** – SyncManager copy logic.
|
||||
7. **Phase 4.1+4.2** – TrayApp shell with SignalR client.
|
||||
8. **Phase 4.3** – Project Management Window (path configuration UI).
|
||||
9. **Phase 4.4** – Pending Changes Window.
|
||||
|
||||
Use `CommunityToolkit.Mvvm` for `[ObservableProperty]`, `[RelayCommand]` source generators in all ViewModels.
|
||||
|
||||
Ensure all code compiles under .NET 10. Use clean architecture and dependency injection throughout.
|
||||
Reference in New Issue
Block a user