Upgrade Sitzplan - Jetzt mit Bewertungsfeature

This commit is contained in:
2026-08-19 16:46:08 +02:00
parent f514d1d58c
commit 0faa3f54de
14 changed files with 370 additions and 14 deletions
+3
View File
@@ -9,6 +9,7 @@ using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Views;
using LehrerApp.Sync;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop;
@@ -66,6 +67,8 @@ public class App : Application
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
WireCallbacks(mainVm);
var main = new MainWindow { DataContext = mainVm };
if (Services.GetService<SyncEngine>() is { } syncEngine)
main.EnableFinalSync(syncEngine);
desktop.MainWindow = main;
if (showImmediately) main.Show();
}
@@ -31,6 +31,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
[ObservableProperty] private StudentOption _selectedStudentFilter = AllStudentsOption;
[ObservableProperty] private bool _onlyThisGroup;
[ObservableProperty] private int _draftCount;
public Func<Guid, List<StudentOption>, Documentation?, Task<Documentation?>>? OnEditDocumentation { get; set; }
public Func<DocumentationItem, Task<bool>>? OnConfirmDeleteDocumentation { get; set; }
@@ -80,7 +81,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
var all = relevantStudentIds
.SelectMany(id => _docs.GetByStudent(id))
.Where(d => !OnlyThisGroup || d.GroupId == _groupId)
.OrderByDescending(d => d.Date);
.OrderByDescending(d => d.IsDraft)
.ThenByDescending(d => d.Date)
.ToList();
DraftCount = all.Count(d => d.IsDraft && (d.GroupId is null || d.GroupId == _groupId));
foreach (var d in all)
{
@@ -91,6 +95,8 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
}
}
public void Refresh() => Load();
[RelayCommand]
private async Task AddDocumentation()
{
@@ -250,6 +250,7 @@ public partial class GroupDetailViewModel : ObservableObject
ParticipationTab.LoadSessions();
ParticipationTab.RefreshCurrentGrid();
};
SeatingPlanTab.OnDocumentationChanged = GroupDocumentationTab.Refresh;
}
public void LoadGroup(Guid id)
@@ -15,6 +15,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
private readonly IParticipationSessionRepository _sessions;
private readonly IParticipationRepository _participation;
private readonly IParticipationAspectRepository _aspects;
private readonly IDocumentationRepository? _documentation;
private Guid _groupId;
private SeatingPlan? _currentPlan;
private bool _isReadOnly;
@@ -28,11 +29,13 @@ public partial class SeatingPlanTabViewModel : ObservableObject
[ObservableProperty] private bool _isBoardAtBottom;
[ObservableProperty] private IReadOnlyList<double> _columnGapWidths = [];
[ObservableProperty] private bool _isEditMode;
[ObservableProperty] private ParticipationSessionOption? _selectedSession;
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
public ObservableCollection<StudentSeatOption> UnassignedStudents { get; } = [];
public ObservableCollection<ParticipationSessionOption> TodaySessions { get; } = [];
public bool HasPlans => Plans.Count > 0;
public bool HasSelectedPlan => _currentPlan is not null;
@@ -42,10 +45,12 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
public Func<SeatAssessmentViewModel, Task>? OnAssessStudent { get; set; }
public Action? OnAssessmentChanged { get; set; }
public Action? OnDocumentationChanged { get; set; }
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
IGroupMembershipRepository memberships, IParticipationSessionRepository sessions,
IParticipationRepository participation, IParticipationAspectRepository aspects)
IParticipationRepository participation, IParticipationAspectRepository aspects,
IDocumentationRepository? documentation = null)
{
_plans = plans;
_students = students;
@@ -53,6 +58,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
_sessions = sessions;
_participation = participation;
_aspects = aspects;
_documentation = documentation;
}
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
@@ -63,12 +69,31 @@ public partial class SeatingPlanTabViewModel : ObservableObject
_groupId = groupId;
_isReadOnly = isReadOnly;
IsEditMode = false;
LoadTodaySessions();
LoadStudentOptions();
ReloadPlans();
OnPropertyChanged(nameof(IsEditable));
NotifyCommands();
}
private void LoadTodaySessions()
{
TodaySessions.Clear();
var today = DateOnly.FromDateTime(DateTime.Today);
var sessions = _sessions.GetByGroup(_groupId).Where(s => s.Date == today)
.OrderBy(s => s.CreatedAt).ToList();
if (sessions.Count == 0 && !_isReadOnly)
{
var created = new ParticipationSession { GroupId = _groupId, Date = today, Comment = "Sitzplan" };
_sessions.Save(created);
sessions.Add(created);
}
foreach (var session in sessions) TodaySessions.Add(new ParticipationSessionOption(session));
SelectedSession = TodaySessions.LastOrDefault();
}
partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
private void LoadStudentOptions()
{
StudentOptions.Clear();
@@ -133,14 +158,78 @@ public partial class SeatingPlanTabViewModel : ObservableObject
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
CanEditLayout, ToggleSituationTag, IsEditable));
}
UpdateAssignmentSummary();
RefreshSeatLessonData();
}
OnPropertyChanged(nameof(HasSelectedPlan));
NotifyCommands();
}
private void RefreshSeatLessonData()
{
if (SelectedSession is null)
{
foreach (var seat in Seats) seat.SetLessonData(null, []);
return;
}
var entries = _participation.GetBySession(SelectedSession.Id)
.ToDictionary(e => e.StudentId);
foreach (var seat in Seats)
{
if (seat.SelectedOption.StudentId is not Guid studentId)
{
seat.SetLessonData(null, []);
continue;
}
entries.TryGetValue(studentId, out var entry);
var tags = _documentation?.GetByStudent(studentId)
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id)?.Tags ?? [];
seat.SetLessonData(entry, tags);
}
}
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
{
if (!IsEditable || _documentation is null || SelectedSession is null ||
seat.SelectedOption.StudentId is not Guid studentId) return;
var draft = _documentation.GetByStudent(studentId)
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id);
if (draft is null)
{
draft = new Documentation
{
StudentId = studentId, GroupId = _groupId,
ParticipationSessionId = SelectedSession.Id,
LessonId = SelectedSession.LessonId,
Type = DocumentationType.Incident,
Date = SelectedSession.Date,
Title = tag,
IsDraft = true,
Tags = [tag],
};
}
else if (draft.Tags.Contains(tag))
{
draft.Tags.Remove(tag);
if (draft.Tags.Count == 0)
{
_documentation.Delete(draft.Id);
RefreshSeatLessonData();
OnDocumentationChanged?.Invoke();
return;
}
draft.Title = draft.Tags[0];
}
else draft.Tags.Add(tag);
draft.UpdatedAt = DateTime.UtcNow;
_documentation.Save(draft);
RefreshSeatLessonData();
OnDocumentationChanged?.Invoke();
}
private void OnSeatChanged(SeatCellViewModel changed)
{
if (_currentPlan is null || !CanEditLayout) return;
@@ -209,8 +298,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
{
if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return;
var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects,
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable);
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable,
SelectedSession?.Id);
await OnAssessStudent(assessment);
RefreshSeatLessonData();
OnAssessmentChanged?.Invoke();
}
@@ -241,7 +332,11 @@ public partial class SeatingPlanTabViewModel : ObservableObject
partial void OnIsEditModeChanged(bool value)
{
OnPropertyChanged(nameof(CanEditLayout));
foreach (var seat in Seats) seat.CanEdit = CanEditLayout;
foreach (var seat in Seats)
{
seat.CanEdit = CanEditLayout;
seat.CanRecordLesson = IsEditable && !value;
}
NotifyCommands();
}
@@ -275,10 +370,21 @@ public sealed record StudentSeatOption(Guid? StudentId, string DisplayName)
public static StudentSeatOption Empty { get; } = new(null, "— frei —");
}
public sealed class ParticipationSessionOption(ParticipationSession session)
{
public Guid Id => session.Id;
public Guid? LessonId => session.LessonId;
public DateOnly Date => session.Date;
public string DisplayName => string.IsNullOrWhiteSpace(session.Comment)
? $"{session.Date:dd.MM.yyyy}"
: $"{session.Date:dd.MM.yyyy} · {session.Comment}";
}
public partial class SeatCellViewModel : ObservableObject
{
private readonly Action<SeatCellViewModel> _onChanged;
private bool _suppressChange;
private readonly Action<SeatCellViewModel, string> _toggleSituationTag;
[ObservableProperty] private StudentSeatOption _selectedOption;
[ObservableProperty] private bool _isDropTarget;
@@ -287,11 +393,20 @@ public partial class SeatCellViewModel : ObservableObject
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
public ObservableCollection<StudentSeatOption> Options { get; }
[ObservableProperty] private bool _canEdit;
[ObservableProperty] private double _lessonOpacity = 1.0;
[ObservableProperty] private string _attendanceBadge = "";
[ObservableProperty] private string _homeworkBadge = "";
public ObservableCollection<SituationTagChoice> SituationTags { get; } = [];
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
public bool ShowLessonOverview => IsOccupied && !CanEdit;
[ObservableProperty] private bool _canRecordLesson;
public bool IsOccupied => SelectedOption.StudentId.HasValue;
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit)
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false)
{
Row = row;
Column = column;
@@ -299,21 +414,54 @@ public partial class SeatCellViewModel : ObservableObject
_selectedOption = selectedOption;
_onChanged = onChanged;
_canEdit = canEdit;
_toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
_canRecordLesson = canRecordLesson;
foreach (var tag in SituationTagChoice.DefaultTags)
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
}
partial void OnSelectedOptionChanged(StudentSeatOption value)
{
OnPropertyChanged(nameof(IsOccupied));
OnPropertyChanged(nameof(StudentName));
OnPropertyChanged(nameof(ShowLessonOverview));
if (!_suppressChange) _onChanged(this);
}
partial void OnCanEditChanged(bool value) => OnPropertyChanged(nameof(ShowLessonOverview));
public void SetSelectionSilently(StudentSeatOption option)
{
_suppressChange = true;
SelectedOption = option;
_suppressChange = false;
}
public void SetLessonData(ParticipationEntry? entry, IEnumerable<string> tags)
{
var attendance = entry?.Attendance;
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
LessonOpacity = attendance is not null and not AttendanceStatus.Present
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
OnPropertyChanged(nameof(HasAttendanceBadge));
OnPropertyChanged(nameof(HasHomeworkBadge));
}
}
public partial class SituationTagChoice(string text, bool isSelected, Action<string> toggle) : ObservableObject
{
public static readonly string[] DefaultTags =
[
"Mitarbeit verweigert", "Unterricht gestört", "Streit/Konflikt",
"Langer Toilettengang", "Material vergessen", "Handynutzung",
"Besonders hilfsbereit", "Sehr gute Mitarbeit", "Gespräch erforderlich",
];
public string Text { get; } = text;
[ObservableProperty] private bool _isSelected = isSelected;
[RelayCommand] private void Toggle() => toggle(Text);
}
public partial class SeatAssessmentViewModel : ObservableObject
@@ -338,13 +486,14 @@ public partial class SeatAssessmentViewModel : ObservableObject
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
IParticipationRepository entries, IParticipationAspectRepository aspects,
Guid groupId, Guid studentId, string studentName, bool canEdit)
Guid groupId, Guid studentId, string studentName, bool canEdit, Guid? sessionId = null)
{
_entries = entries;
_canEdit = canEdit;
StudentName = studentName;
var today = DateOnly.FromDateTime(DateTime.Today);
var session = sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
var session = sessionId.HasValue ? sessions.GetById(sessionId.Value) : null;
session ??= sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
if (session is null && canEdit)
{
session = new ParticipationSession
@@ -339,6 +339,9 @@ public partial class DocumentationDialogViewModel : ObservableObject
Result.Date = date;
Result.Title = Title.Trim();
Result.Content = (Content ?? "").Trim();
// Das bewusste Speichern im vollständigen Dialog schließt einen im Sitzplan erzeugten
// Schnellentwurf ab. Stunden- und Lesson-Bezug bleiben am bestehenden Objekt erhalten.
Result.IsDraft = false;
Result.IsConfidential = IsConfidential;
Result.Participants = type == DocumentationType.Conversation ? Participants.ToList() : [];
Result.AbsenceData = type == DocumentationType.Absence
@@ -421,6 +424,7 @@ public partial class DocumentationItem : ObservableObject
public bool IsConfidential { get; }
public bool IsParentCall { get; }
public bool HasAttachments { get; }
public bool IsDraft { get; }
public string StatusLabel { get; }
public List<TagChip> TagChips { get; }
/// Nur im Gruppen-Tab (5.1, GroupDocumentationTabViewModel) gefüllt — die Schüler-Detailansicht
@@ -446,6 +450,7 @@ public partial class DocumentationItem : ObservableObject
IsRevealed = !d.IsConfidential;
IsParentCall = d.Type == DocumentationType.ParentCall;
HasAttachments = d.Attachments.Count > 0;
IsDraft = d.IsDraft;
StatusLabel = BuildStatusLabel(d);
TagChips = d.Tags.Select(t => new TagChip(t)).ToList();
StudentName = studentName;
@@ -455,6 +460,7 @@ public partial class DocumentationItem : ObservableObject
private static string BuildStatusLabel(Documentation d) => d.Type switch
{
_ when d.IsDraft => "Nacharbeiten",
DocumentationType.ParentCall when d.ParentCallData is { IsConducted: true } pc =>
$"Durchgeführt am {pc.ConductedDate:dd.MM.yyyy}",
DocumentationType.ParentCall => "Noch nicht durchgeführt",
@@ -7,7 +7,7 @@
<Grid RowDefinitions="Auto,*" Margin="16">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Margin="0,0,0,12">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,*,Auto" Margin="0,0,0,12">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" Margin="0,0,12,0">
<TextBlock Text="Schüler:" VerticalAlignment="Center"/>
<ComboBox ItemsSource="{Binding StudentFilterOptions}" SelectedItem="{Binding SelectedStudentFilter}"
@@ -16,7 +16,12 @@
<CheckBox Grid.Column="1" Content="Nur dieser Unterricht" IsChecked="{Binding OnlyThisGroup}"
VerticalAlignment="Center"
ToolTip.Tip="Standardmäßig werden auch Einträge aus anderen Lerngruppen desselben Schülers angezeigt (optisch abgesetzt) — damit Muster über mehrere Fächer/Kurse hinweg sichtbar bleiben."/>
<Button Grid.Column="3" Content=" Eintrag" Command="{Binding AddDocumentationCommand}"/>
<Border Grid.Column="2" Background="#FB8C00" CornerRadius="11" Padding="9,3" Margin="12,0"
IsVisible="{Binding !!DraftCount}">
<TextBlock Text="{Binding DraftCount, StringFormat='Nacharbeiten: {0}'}" Foreground="White"
FontSize="11" FontWeight="SemiBold"/>
</Border>
<Button Grid.Column="4" Content=" Eintrag" Command="{Binding AddDocumentationCommand}"/>
</Grid>
<ScrollViewer Grid.Row="1">
@@ -32,6 +37,10 @@
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" Opacity="0.5" FontSize="12"/>
<StackPanel Grid.Column="1" Margin="8,0">
<StackPanel Orientation="Horizontal" Spacing="6">
<Border Background="#FB8C00" CornerRadius="8" Padding="6,1"
IsVisible="{Binding IsDraft}">
<TextBlock Text="ENTWURF" Foreground="White" FontSize="9" FontWeight="Bold"/>
</Border>
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"/>
<TextBlock Text="{Binding Model.Title}" FontSize="13" Opacity="0.8"
IsVisible="{Binding IsRevealed}"/>
@@ -66,6 +66,12 @@
<StackPanel Spacing="3">
<TextBlock Text="{Binding PlanTitle}" FontSize="22" FontWeight="SemiBold"/>
<TextBlock Text="{Binding PlanSubtitle}" Opacity="0.65"/>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0"
IsVisible="{Binding IsEditMode, Converter={x:Static BoolConverters.Not}}">
<TextBlock Text="Unterricht:" VerticalAlignment="Center" FontSize="12" Opacity="0.65"/>
<ComboBox ItemsSource="{Binding TodaySessions}" SelectedItem="{Binding SelectedSession}"
DisplayMemberBinding="{Binding DisplayName}" MinWidth="210"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Bottom" Spacing="4">
<ToggleSwitch Content="Bearbeitungsmodus" IsChecked="{Binding IsEditMode}"
@@ -112,7 +118,7 @@
PointerPressed="OnDragSourcePressed"
PointerMoved="OnDragSourceMoved"
PointerReleased="OnDragSourceReleased"
Tapped="OnSeatTapped">
Tapped="OnSeatTapped" Opacity="{Binding LessonOpacity}">
<StackPanel Spacing="5">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
@@ -120,6 +126,33 @@
</Grid>
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5"
IsVisible="{Binding ShowLessonOverview}">
<Border Background="#1976D2" CornerRadius="8" Padding="6,1"
IsVisible="{Binding HasAttendanceBadge}">
<TextBlock Text="{Binding AttendanceBadge}" Foreground="White" FontSize="10"
ToolTip.Tip="Anwesenheit"/>
</Border>
<Border Background="#FB8C00" CornerRadius="8" Padding="6,1"
IsVisible="{Binding HasHomeworkBadge}">
<TextBlock Text="{Binding HomeworkBadge}" Foreground="White" FontSize="10"
ToolTip.Tip="Hausaufgabe"/>
</Border>
</StackPanel>
<Expander Header=" Situation" FontSize="10"
IsVisible="{Binding CanRecordLesson}">
<ItemsControl ItemsSource="{Binding SituationTags}" Margin="0,4,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel ItemSpacing="3" LineSpacing="3"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:SituationTagChoice">
<ToggleButton Content="{Binding Text}" IsChecked="{Binding IsSelected, Mode=OneWay}"
Command="{Binding ToggleCommand}" FontSize="9" Padding="5,2"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</StackPanel>
</Border>
</DataTemplate>
@@ -1,7 +1,9 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Shared;
@@ -195,6 +197,9 @@ public partial class SeatingPlanTabView : UserControl
private async void OnSeatTapped(object? sender, TappedEventArgs e)
{
if (e.Source is Control source &&
(source is Button or Expander || source.FindAncestorOfType<Button>() is not null ||
source.FindAncestorOfType<Expander>() is not null)) return;
if (DateTime.UtcNow < _ignoreTapUntil || sender is not Border { DataContext: SeatCellViewModel seat }
|| !seat.IsOccupied || DataContext is not SeatingPlanTabViewModel vm) return;
await vm.AssessStudent(seat);
@@ -1,11 +1,17 @@
using Avalonia.Controls;
using Avalonia.Input;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Sync;
namespace LehrerApp.Desktop.Views;
public partial class MainWindow : Window
{
private static readonly TimeSpan FinalSyncDelay = TimeSpan.FromMilliseconds(350);
private SyncEngine? _syncEngine;
private bool _finalSyncStarted;
private bool _closeAfterFinalSync;
public MainWindow()
{
InitializeComponent();
@@ -14,6 +20,24 @@ public partial class MainWindow : Window
KeyDown += (_, _) => NotifyActivity();
}
public void EnableFinalSync(SyncEngine syncEngine)
{
_syncEngine = syncEngine;
Closing += OnClosing;
}
private async void OnClosing(object? sender, WindowClosingEventArgs e)
{
if (_closeAfterFinalSync || _finalSyncStarted || _syncEngine is null)
return;
e.Cancel = true;
_finalSyncStarted = true;
await _syncEngine.SyncBeforeShutdownAsync(FinalSyncDelay);
_closeAfterFinalSync = true;
Close();
}
private void NotifyActivity()
{
if (DataContext is MainWindowViewModel vm) vm.AppLock.NotifyActivity();