205 lines
8.1 KiB
C#
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);
|
|
}
|
|
}
|