This commit is contained in:
2026-03-29 23:47:31 +02:00
commit 216d5d2280
75 changed files with 5702 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Data.Repositories;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Sync;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop;
public static class AppBootstrapper
{
public static IServiceProvider BuildServices()
{
var services = new ServiceCollection();
// ── Pfade ─────────────────────────────────────────────────────────────
var appData = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"LehrerApp");
Directory.CreateDirectory(appData);
var dbPath = Path.Combine(appData, "lehrerapp.db");
var queuePath = Path.Combine(appData, "syncqueue.db");
var keyPath = Path.Combine(appData, "sync.key");
// ── Datenbank ─────────────────────────────────────────────────────────
services.AddSingleton(_ => new LiteDbContext(dbPath));
// ── Repositories ──────────────────────────────────────────────────────
services.AddSingleton<IStudentRepository, StudentRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<IEnrollmentRepository, EnrollmentRepository>();
services.AddSingleton<IExamRepository, ExamRepository>();
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
services.AddSingleton<IGradeRepository, GradeRepository>();
services.AddSingleton<IUnitRepository, UnitRepository>();
services.AddSingleton<ILessonRepository, LessonRepository>();
services.AddSingleton<IDocumentationRepository, DocumentationRepository>();
services.AddSingleton<IWorkTaskRepository, WorkTaskRepository>();
services.AddSingleton<ITimeEntryRepository, TimeEntryRepository>();
// ── Core Services ─────────────────────────────────────────────────────
services.AddSingleton<GradingService>();
services.AddSingleton<SchoolYearService>();
// ── Sync ──────────────────────────────────────────────────────────────
services.AddSingleton(_ => new EventQueue(queuePath));
services.AddSingleton(sp =>
new ConflictResolver(sp.GetRequiredService<EventQueue>()));
// Schlüssel laden oder neu generieren
services.AddSingleton<byte[]>(_ =>
{
var key = SyncCrypto.LoadKey(keyPath) ?? SyncCrypto.GenerateKey();
SyncCrypto.SaveKey(key, keyPath);
return key;
});
// EventApplier wendet gezogene Events auf LiteDB an
services.AddSingleton<EventApplier>(sp => new EventApplier(
key: sp.GetRequiredService<byte[]>(),
students: sp.GetRequiredService<IStudentRepository>(),
groups: sp.GetRequiredService<IGroupRepository>(),
enrollments: sp.GetRequiredService<IEnrollmentRepository>(),
exams: sp.GetRequiredService<IExamRepository>(),
examResults: sp.GetRequiredService<IExamResultRepository>(),
grades: sp.GetRequiredService<IGradeRepository>(),
units: sp.GetRequiredService<IUnitRepository>(),
lessons: sp.GetRequiredService<ILessonRepository>(),
docs: sp.GetRequiredService<IDocumentationRepository>(),
tasks: sp.GetRequiredService<IWorkTaskRepository>(),
timeEntries: sp.GetRequiredService<ITimeEntryRepository>()));
// SyncEngine + ReadableSnapshotService nur wenn Server konfiguriert
var serverUrl = LoadServerUrl(appData);
var deviceId = LoadOrCreateDeviceId(appData);
services.AddSingleton<SyncEngine?>(sp =>
{
if (string.IsNullOrEmpty(serverUrl)) return null;
var http = BuildHttpClient(serverUrl, appData);
var config = new SyncConfig
{
ServerUrl = serverUrl,
DeviceId = deviceId,
DeviceType = DeviceType.Desktop,
AutoSyncIntervalMinutes = 5,
};
return new SyncEngine(
sp.GetRequiredService<EventQueue>(),
sp.GetRequiredService<ConflictResolver>(),
http,
config);
});
services.AddSingleton<ReadableSnapshotService?>(sp =>
{
if (string.IsNullOrEmpty(serverUrl)) return null;
return new ReadableSnapshotService(
http: BuildHttpClient(serverUrl, appData),
students: sp.GetRequiredService<IStudentRepository>(),
groups: sp.GetRequiredService<IGroupRepository>(),
enrollments: sp.GetRequiredService<IEnrollmentRepository>(),
exams: sp.GetRequiredService<IExamRepository>(),
examResults: sp.GetRequiredService<IExamResultRepository>(),
grades: sp.GetRequiredService<IGradeRepository>(),
units: sp.GetRequiredService<IUnitRepository>(),
lessons: sp.GetRequiredService<ILessonRepository>(),
tasks: sp.GetRequiredService<IWorkTaskRepository>(),
schoolYear: sp.GetRequiredService<SchoolYearService>(),
deviceId: deviceId);
});
services.AddSingleton<SnapshotService?>(sp =>
{
if (string.IsNullOrEmpty(serverUrl)) return null;
return new SnapshotService(
http: BuildHttpClient(serverUrl, appData),
db: sp.GetRequiredService<LiteDbContext>(),
syncKey: sp.GetRequiredService<byte[]>(),
deviceType: DeviceType.Desktop,
dbPath: dbPath,
keyPath: keyPath);
});
// ── ViewModels ────────────────────────────────────────────────────────
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<DashboardViewModel>();
services.AddTransient<GroupListViewModel>();
services.AddTransient<GroupDetailViewModel>();
services.AddTransient<StudentListViewModel>();
services.AddTransient<StudentDetailViewModel>();
services.AddSingleton<SyncStatusViewModel>();
services.AddTransient<DevicePairingViewModel>(sp =>
new DevicePairingViewModel(
sp.GetRequiredService<SnapshotService?>()
?? throw new InvalidOperationException(
"Kein Server konfiguriert.")));
return services.BuildServiceProvider();
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static HttpClient BuildHttpClient(string serverUrl, string appData)
{
var http = new HttpClient { BaseAddress = new Uri(serverUrl) };
var tokenPath = Path.Combine(appData, "auth.token");
if (File.Exists(tokenPath))
{
var token = File.ReadAllText(tokenPath).Trim();
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", token);
}
return http;
}
private static string LoadServerUrl(string appData)
{
var path = Path.Combine(appData, "server.txt");
return File.Exists(path) ? File.ReadAllText(path).Trim() : "";
}
private static string LoadOrCreateDeviceId(string appData)
{
var path = Path.Combine(appData, "device.id");
if (File.Exists(path)) return File.ReadAllText(path).Trim();
var id = Guid.NewGuid().ToString();
File.WriteAllText(path, id);
return id;
}
}