Files
LehrerApp/LehrerApp.Desktop/Views/Groups/StudentGroupGeneratorWindow.axaml.cs
T
2026-09-29 22:17:49 +02:00

205 lines
8.1 KiB
C#

using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Labs.Lottie;
using Avalonia.Threading;
using LehrerApp.Core.Interfaces;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
public partial class StudentGroupGeneratorWindow : Window
{
private bool _isSpinning;
private DateTime _spinStartedAt;
private StudentGroupMember? _dragCandidate;
private PointerPressedEventArgs? _dragTrigger;
private Avalonia.Point _pressPosition;
private StudentGroupMember? _draggedMember;
private GeneratedStudentGroup? _pendingDropTarget;
public StudentGroupGeneratorWindow() => InitializeComponent();
public static StudentGroupGeneratorWindow Create(Guid? initialGroupId, bool useTodayAttendance = false)
{
var vm = new StudentGroupGeneratorViewModel(
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IGroupRepository>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<IParticipationSessionRepository>(),
App.Services.GetRequiredService<IParticipationRepository>(),
App.Services.GetRequiredService<IGradeRepository>(),
initialGroupId,
savedPlans: App.Services.GetRequiredService<IStudentGroupPlanRepository>());
if (useTodayAttendance)
vm.ParticipantModeName = StudentGroupGeneratorViewModel.AdHocModeLabel;
return new StudentGroupGeneratorWindow { DataContext = vm };
}
private async void OnGenerate(object? sender, RoutedEventArgs e)
{
if (DataContext is not StudentGroupGeneratorViewModel vm ||
!vm.GenerateCommand.CanExecute(null)) return;
var results = this.FindControl<ScrollViewer>("ResultsScrollViewer");
var teamwork = this.FindControl<Lottie>("TeamworkAnimation");
var button = this.FindControl<Button>("GenerateButton");
var options = this.FindControl<Expander>("OptionsExpander");
var summary = this.FindControl<StackPanel>("ResultSummary");
var actions = this.FindControl<StackPanel>("PlanActions");
if (results is null || teamwork is null || button is null || options is null ||
summary is null || actions is null) return;
if (!_isSpinning)
{
_isSpinning = true;
_spinStartedAt = DateTime.UtcNow;
results.IsVisible = false;
actions.IsVisible = false;
summary.IsVisible = false;
teamwork.IsVisible = true;
options.IsEnabled = false;
button.Content = "Teamwork läuft …";
// Nach dem Layoutwechsel starten, damit der erste Frame sichtbar ist.
await Task.Yield();
if (!_isSpinning) return;
teamwork.Start();
button.Content = "Ergebnis anzeigen";
return;
}
// Ein Doppelklick soll die Animation nicht sofort wieder schließen.
if (DateTime.UtcNow - _spinStartedAt < TimeSpan.FromSeconds(1.2)) return;
try
{
vm.GenerateCommand.Execute(null);
}
finally
{
teamwork.Stop();
teamwork.IsVisible = false;
results.IsVisible = true;
actions.IsVisible = true;
summary.IsVisible = true;
options.IsEnabled = true;
button.Content = "Gruppen zusammenstellen";
_isSpinning = false;
}
}
private void OnMemberPressed(object? sender, PointerPressedEventArgs e)
{
if (_isSpinning || sender is not Control { DataContext: StudentGroupMember member } control ||
!e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return;
_dragCandidate = member;
_dragTrigger = e;
_pressPosition = e.GetPosition(this);
}
private async void OnLoadSavedPlan(object? sender, RoutedEventArgs e)
{
if (DataContext is not StudentGroupGeneratorViewModel vm ||
!vm.LoadSavedPlanCommand.CanExecute(null)) return;
if (vm.HasUnsavedChanges)
{
var dialog = new ConfirmDialog
{
DataContext = new ConfirmDialogInfo
{
Title = "Ungespeicherte Einteilung ersetzen?",
Message = "Die aktuelle Einteilung und manuelle Änderungen werden beim Laden verworfen.",
ConfirmText = "Gespeicherte Einteilung laden",
},
};
if (!await dialog.ShowDialog<bool>(this)) return;
}
vm.LoadSavedPlanCommand.Execute(null);
}
private async void OnDeleteSavedPlan(object? sender, RoutedEventArgs e)
{
if (DataContext is not StudentGroupGeneratorViewModel vm ||
!vm.DeleteSavedPlanCommand.CanExecute(null)) return;
var dialog = new ConfirmDialog
{
DataContext = new ConfirmDialogInfo
{
Title = "Einteilung löschen?",
Message = $"Die gespeicherte Einteilung „{vm.SelectedSavedPlan!.Name}“ wird gelöscht.",
ConfirmText = "Löschen",
},
};
if (await dialog.ShowDialog<bool>(this)) vm.DeleteSavedPlanCommand.Execute(null);
}
private async void OnMemberMoved(object? sender, PointerEventArgs e)
{
if (_dragCandidate is null || _dragTrigger is null ||
!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
var position = e.GetPosition(this);
if (Math.Abs(position.X - _pressPosition.X) < 6 &&
Math.Abs(position.Y - _pressPosition.Y) < 6) return;
_draggedMember = _dragCandidate;
_pendingDropTarget = null;
_dragCandidate = null;
var trigger = _dragTrigger;
_dragTrigger = null;
var data = new DataTransfer();
data.Add(DataTransferItem.CreateText("LehrerApp-Gruppeneinteilung"));
var effect = await DragDrop.DoDragDropAsync(trigger, data, DragDropEffects.Move);
var member = _draggedMember;
var target = _pendingDropTarget;
_draggedMember = null;
_pendingDropTarget = null;
if ((effect & DragDropEffects.Move) == 0 || member is null || target is null) return;
// Die Quelle erst verändern, wenn Avalonia den Drop vollständig verarbeitet hat.
Dispatcher.UIThread.Post(() =>
{
if (DataContext is StudentGroupGeneratorViewModel vm) vm.MoveStudent(member, target);
}, DispatcherPriority.Background);
}
private void OnMemberReleased(object? sender, PointerReleasedEventArgs e)
{
_dragCandidate = null;
_dragTrigger = null;
}
private bool CanDropOn(GeneratedStudentGroup target) =>
!_isSpinning && _draggedMember is { } member &&
!target.Members.Any(existing => existing.Id == member.Id);
private void OnGroupDragEnter(object? sender, DragEventArgs e)
{
if (sender is Border { DataContext: GeneratedStudentGroup target } && CanDropOn(target))
target.IsDropTarget = true;
}
private void OnGroupDragLeave(object? sender, DragEventArgs e)
{
if (sender is Border { DataContext: GeneratedStudentGroup target }) target.IsDropTarget = false;
}
private void OnGroupDragOver(object? sender, DragEventArgs e) =>
e.DragEffects = sender is Border { DataContext: GeneratedStudentGroup target } && CanDropOn(target)
? DragDropEffects.Move : DragDropEffects.None;
private void OnGroupDrop(object? sender, DragEventArgs e)
{
if (sender is not Border { DataContext: GeneratedStudentGroup target }) return;
target.IsDropTarget = false;
if (!CanDropOn(target)) { e.DragEffects = DragDropEffects.None; return; }
_pendingDropTarget = target;
e.DragEffects = DragDropEffects.Move;
}
protected override void OnClosed(EventArgs e)
{
_isSpinning = false;
this.FindControl<Lottie>("TeamworkAnimation")?.Stop();
base.OnClosed(e);
}
}