Added first code.

Signed-off-by: Naoriel Sa' Rocí <naoriel@sa-roci.de>
This commit is contained in:
2026-03-30 02:26:31 +02:00
parent bbf68e1621
commit 4a4fb54143
25 changed files with 1072 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
using ReactiveUI;
using System.Reactive;
using EinmaleinsTrainer.Services;
using EinmaleinsTrainer.Models;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System;
namespace EinmaleinsTrainer.ViewModels;
public class MainViewModel : ViewModelBase
{
public ObservableCollection<int> SelectedRows { get; } = new();
public event EventHandler? RedrawRequired;
private bool _includeSquares;
public bool IncludeSquares
{
get => _includeSquares;
set => this.RaiseAndSetIfChanged(ref _includeSquares, value);
}
private bool _useTimer;
public bool UseTimer
{
get => _useTimer;
set
{
this.RaiseAndSetIfChanged(ref _useTimer, value);
RedrawRequired?.Invoke(this, new());
}
}
private int _secondsPerQuestion = 0;
public int SecondsPerQuestion
{
get => _secondsPerQuestion;
set
{
if (value < 2) value = 2;
if (value > 120) value = 120;
this.RaiseAndSetIfChanged(ref _secondsPerQuestion, value);
}
}
public ReactiveCommand<Unit, QuizModel> StartCommand { get; }
public MainViewModel()
{
StartCommand = ReactiveCommand.Create(
execute: () =>
{
var generator = new QuestionGenerator();
var questions = generator.Generate(SelectedRows.ToList(), IncludeSquares);
return new QuizModel(){ Questions = questions, UseTimer = UseTimer, Seconds = SecondsPerQuestion};
},
canExecute: Observable.Return(true).ObserveOn(RxApp.MainThreadScheduler),
outputScheduler: RxApp.MainThreadScheduler);
}
public void ToggleRow(int row, bool isChecked)
{
if (isChecked)
SelectedRows.Add(row);
else
SelectedRows.Remove(row);
}
public bool IsRow1Checked
{
get => SelectedRows.Contains(1);
set => ToggleRow(1, value);
}
public bool IsRow2Checked
{
get => SelectedRows.Contains(2);
set => ToggleRow(2, value);
}
public bool IsRow3Checked
{
get => SelectedRows.Contains(3);
set => ToggleRow(3, value);
}
public bool IsRow4Checked
{
get => SelectedRows.Contains(4);
set => ToggleRow(4, value);
}
public bool IsRow5Checked
{
get => SelectedRows.Contains(5);
set => ToggleRow(5, value);
}
public bool IsRow6Checked
{
get => SelectedRows.Contains(6);
set => ToggleRow(6, value);
}
public bool IsRow7Checked
{
get => SelectedRows.Contains(7);
set => ToggleRow(7, value);
}
public bool IsRow8Checked
{
get => SelectedRows.Contains(8);
set => ToggleRow(8, value);
}
public bool IsRow9Checked
{
get => SelectedRows.Contains(9);
set => ToggleRow(9, value);
}
public bool IsRow10Checked
{
get => SelectedRows.Contains(10);
set => ToggleRow(10, value);
}
public void Init()
{
SelectedRows.Clear();
IncludeSquares = false;
UseTimer = false;
SecondsPerQuestion = 30;
}
}
+73
View File
@@ -0,0 +1,73 @@
using ReactiveUI;
using System;
using System.Reactive.Linq;
namespace EinmaleinsTrainer.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
private ViewModelBase _current;
public ViewModelBase Current
{
get => _current;
set => SetProperty(ref _current, value);
}
public readonly MainViewModel MainModel = new();
public readonly QuizViewModel QuizModel = new();
public readonly ResultViewModel ResultModel = new();
private readonly ViewModelBase _dummy = new();
public MainWindowViewModel()
{
MainModel.Init();
_current = MainModel;
ResultModel.RestartCommand
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(result =>
{
// In order to keep the last value if required
int secondsPerQuestion = MainModel.SecondsPerQuestion;
MainModel.Init();
if (result.TimeRequired != null)
{
MainModel.SecondsPerQuestion = (result.Score >= 10) ?
(int)Math.Round(0.15 * result.TimeRequired.Value.TotalSeconds, 0, MidpointRounding.AwayFromZero) :
secondsPerQuestion;
MainModel.UseTimer = true;
}
Current = MainModel;
});
QuizModel.Finished
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(result =>
{
ResultModel.Init(result);
Current = ResultModel;
});
QuizModel.RedrawRequired += (s, a) =>
{
Current = _dummy;
Current = QuizModel;
};
MainModel.StartCommand
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(quiz =>
{
if (quiz.Questions.Count == 0) return;
QuizModel.Init(quiz);
Current = QuizModel;
});
MainModel.RedrawRequired += (s, a) =>
{
Current = _dummy;
Current = MainModel;
};
}
}
+293
View File
@@ -0,0 +1,293 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using EinmaleinsTrainer.Models;
namespace EinmaleinsTrainer.ViewModels;
public class QuizViewModel : ViewModelBase
{
// =========================
// Basisdaten
// =========================
private QuizModel _model = new();
private List<Question> _questions = new();
public List<Question> Questions
{
get => _questions;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _questions, value);
this.RaisePropertyChanged(nameof(Total));
// Wenn sich Questions ändern → Index resetten
Index = 0;
});
}
}
public int Total => Questions.Count;
private int _index;
public int Index
{
get => _index;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
bool changed = _index == value;
this.RaiseAndSetIfChanged(ref _index, value);
if (!changed)
{
this.RaisePropertyChanged();
}
UpdateDerivedProperties();
});
}
}
// =========================
// Abgeleitete Properties
// =========================
private Question _currentQuestion = new() { A = 0, B = 0 };
public Question CurrentQuestion
{
get => _currentQuestion;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _currentQuestion, value);
});
}
}
private int _questionNumber;
public int QuestionNumber
{
get => _questionNumber;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _questionNumber, value);
});
}
}
private string _currentQuestionText = "0 × 0";
public string CurrentQuestionText
{
get => _currentQuestionText;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _currentQuestionText, value);
});
}
}
// =========================
// User Input / Status
// =========================
private string _userAnswer = "";
public string UserAnswer
{
get => _userAnswer;
set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _userAnswer, value);
});
}
}
private int _remaining;
public int RemainingSeconds
{
get => _remaining;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _remaining, value);
if (value > 0)
RedrawRequired?.Invoke(this, new());
});
}
}
private int _score;
public int Score
{
get => _score;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _score, value);
});
}
}
public bool UseTimer { get; private set; }
private TimeSpan TimeRequired = TimeSpan.Zero;
private DateTime Started = DateTime.MinValue;
// =========================
// Commands / Events
// =========================
public ReactiveCommand<Unit, Unit> SubmitCommand { get; }
private readonly Subject<ResultModel> _finished = new();
public IObservable<ResultModel> Finished => _finished.AsObservable();
public event EventHandler? RedrawRequired;
private IDisposable? _timerSub;
// =========================
// ctor
// =========================
public QuizViewModel()
{
SubmitCommand = ReactiveCommand.Create(
Submit,
outputScheduler: RxApp.MainThreadScheduler
);
}
// =========================
// Init
// =========================
public void Init(QuizModel model)
{
_timerSub?.Dispose();
_model = model;
TimeRequired = TimeSpan.Zero;
Questions = new List<Question>(model.Questions);
Score = 0;
UserAnswer = "";
UseTimer = model.UseTimer && model.Seconds > 1;
RemainingSeconds = UseTimer ? model.Seconds : 0;
// Abgeleitete Properties initial setzen
UpdateDerivedProperties();
if (UseTimer)
StartTimer(model.Seconds);
}
// =========================
// Ableitungen zentral
// =========================
private void UpdateDerivedProperties()
{
if (Questions.Count == 0 || Index < 0 || Index >= Questions.Count)
{
CurrentQuestion = new Question { A = 0, B = 0 };
QuestionNumber = 0;
CurrentQuestionText = "0 × 0";
return;
}
var q = Questions[Index];
CurrentQuestion = q; // Direkt zuweisen
QuestionNumber = Index + 1;
CurrentQuestionText = $"{q.A} × {q.B}";
}
// =========================
// Timer
// =========================
private void StartTimer(int seconds)
{
Started = DateTime.Now;
RemainingSeconds = seconds;
_timerSub?.Dispose();
_timerSub = Observable
.Interval(TimeSpan.FromSeconds(1), RxApp.MainThreadScheduler)
.Select(i => seconds - (int)i - 1)
.TakeWhile(s => s >= 0)
.Subscribe(s =>
{
RemainingSeconds = s;
if (s == 0)
SubmitInternal(false);
});
}
// =========================
// Submit
// =========================
private void Submit()
{
bool correct =
int.TryParse(UserAnswer, out int a) &&
a == CurrentQuestion.CorrectAnswer;
SubmitInternal(correct);
}
private void SubmitInternal(bool correct)
{
if (correct)
RxApp.MainThreadScheduler.Schedule(() => Score++);
_timerSub?.Dispose();
if (UseTimer)
TimeRequired += DateTime.Now - Started;
RxApp.MainThreadScheduler.Schedule(() =>
{
Index++;
if (Index >= Questions.Count)
{
_finished.OnNext(new ResultModel
{
Score = Score,
Total = Total,
TimeRequired = UseTimer ? TimeRequired : null
});
return;
}
// UserAnswer zurücksetzen
UserAnswer = "";
// Update der abgeleiteten Properties (CurrentQuestionText, QuestionNumber)
UpdateDerivedProperties();
RedrawRequired?.Invoke(this, new());
});
if (UseTimer && Index < Questions.Count)
StartTimer(_model.Seconds);
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using EinmaleinsTrainer.Models;
using ReactiveUI;
namespace EinmaleinsTrainer.ViewModels;
public class ResultViewModel : ViewModelBase
{
public string Text { get; private set;} = "";
public string TimeRequiredText { get; private set;} = "";
public bool ShowTimeRequired => !string.IsNullOrEmpty(TimeRequiredText);
private ResultModel _model = new();
public void Init(ResultModel result)
{
double percent = result.Score * 1.0 / result.Total;
if (percent > 0.9)
{
Text = $"🎉 {result.Score} von {result.Total} richtig!";
}
else if (percent > 0.5)
{
Text = $"👍 {result.Score} von {result.Total} richtig.";
}
else if (percent > 0.25)
{
Text = $"🤨 {result.Score} von {result.Total} richtig.";
}
else if (percent > 0.0)
{
Text = $"😑 {result.Score} von {result.Total} richtig.";
}
else
{
Text = $"😭 {result.Score} von {result.Total} richtig.";
}
if (result.TimeRequired != null)
{
int SecondsRequired = (int)Math.Round(result.TimeRequired.Value.TotalSeconds, 0, MidpointRounding.AwayFromZero);
TimeRequiredText = $"In {SecondsRequired} s gelöst.";
}
else
{
TimeRequiredText = "";
}
_model = result;
}
public ReactiveCommand<Unit, ResultModel> RestartCommand { get; }
public ResultViewModel()
{
RestartCommand = ReactiveCommand.Create(
execute: () =>
{
return _model;
},
canExecute: Observable.Return(true).ObserveOn(RxApp.MainThreadScheduler),
outputScheduler: RxApp.MainThreadScheduler);
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ReactiveUI;
namespace EinmaleinsTrainer.ViewModels;
public class ViewModelBase : IReactiveObject
{
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;
public void RaisePropertyChanged(PropertyChangedEventArgs args)
{
PropertyChanged?.Invoke(this, args);
}
public void RaisePropertyChanging(PropertyChangingEventArgs args)
{
PropertyChanging?.Invoke(this, args);
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (Equals(field, value)) return false;
OnPropertyChanging(propertyName);
field = value;
OnPropertyChanged(propertyName);
return true;
}
}