diff --git a/.gitignore b/.gitignore
index 8c2b884..f9082ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@
# Built Visual Studio Code Extensions
*.vsix
+bin/*
+obj/*
diff --git a/App.axaml b/App.axaml
new file mode 100644
index 0000000..976b161
--- /dev/null
+++ b/App.axaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/App.axaml.cs b/App.axaml.cs
new file mode 100644
index 0000000..69d6910
--- /dev/null
+++ b/App.axaml.cs
@@ -0,0 +1,28 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using EinmaleinsTrainer.Views;
+using ReactiveUI;
+using Avalonia.ReactiveUI;
+
+namespace EinmaleinsTrainer;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/Assets/avalonia-logo.ico b/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..f7da8bb
Binary files /dev/null and b/Assets/avalonia-logo.ico differ
diff --git a/EinmaleinsTrainer.csproj b/EinmaleinsTrainer.csproj
new file mode 100644
index 0000000..2e03865
--- /dev/null
+++ b/EinmaleinsTrainer.csproj
@@ -0,0 +1,28 @@
+
+
+ WinExe
+ net8.0
+ enable
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
diff --git a/EinmaleinsTrainer.sln b/EinmaleinsTrainer.sln
new file mode 100644
index 0000000..5ee42bd
--- /dev/null
+++ b/EinmaleinsTrainer.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EinmaleinsTrainer", "EinmaleinsTrainer.csproj", "{660FFE01-9483-61B8-28ED-58FAD08A78FD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {660FFE01-9483-61B8-28ED-58FAD08A78FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {660FFE01-9483-61B8-28ED-58FAD08A78FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {660FFE01-9483-61B8-28ED-58FAD08A78FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {660FFE01-9483-61B8-28ED-58FAD08A78FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E30069BC-F120-41C6-970C-3AE4CCC2F5C1}
+ EndGlobalSection
+EndGlobal
diff --git a/Models/Question.cs b/Models/Question.cs
new file mode 100644
index 0000000..6a1d22d
--- /dev/null
+++ b/Models/Question.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace EinmaleinsTrainer.Models;
+
+public class Question : IEquatable
+{
+ public int A { get; set; }
+ public int B { get; set; }
+
+ public int CorrectAnswer => A * B;
+
+ public bool Equals(Question? other)
+ {
+ if (other is null) return false;
+
+ return A == other.A &&
+ B == other.B;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as Question);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(A, B);
+ }
+
+ public override string ToString()
+ {
+ return $"{A} x {B}";
+ }
+}
+
diff --git a/Models/QuizModel.cs b/Models/QuizModel.cs
new file mode 100644
index 0000000..65aa4f9
--- /dev/null
+++ b/Models/QuizModel.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace EinmaleinsTrainer.Models;
+
+public class QuizModel
+{
+ public List Questions { get; set; } = [];
+ public bool UseTimer { get; set; }
+ public int Seconds { get; set; } = 10;
+}
diff --git a/Models/ResultModel.cs b/Models/ResultModel.cs
new file mode 100644
index 0000000..49164a2
--- /dev/null
+++ b/Models/ResultModel.cs
@@ -0,0 +1,9 @@
+using System;
+namespace EinmaleinsTrainer.Models;
+
+public class ResultModel
+{
+ public int Score { get; set; } = 0;
+ public int Total { get; set; } = 0;
+ public TimeSpan? TimeRequired = null;
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..2e3478e
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,23 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+using System;
+
+namespace EinmaleinsTrainer;
+
+sealed class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .UseReactiveUI()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/Services/QuestionGenerator.cs b/Services/QuestionGenerator.cs
new file mode 100644
index 0000000..44a5a6c
--- /dev/null
+++ b/Services/QuestionGenerator.cs
@@ -0,0 +1,33 @@
+using EinmaleinsTrainer.Models;
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace EinmaleinsTrainer.Services;
+
+public class QuestionGenerator
+{
+ private readonly Random _random = new();
+
+ public List Generate(List rows, bool includeSquares)
+ {
+ var pool = new List();
+
+ foreach (var r in rows)
+ {
+ for (int i = 1; i <= 10; i++)
+ pool.Add(new Question { A = r, B = i });
+ }
+
+ if (includeSquares)
+ {
+ for (int i = 1; i <= 10; i++)
+ pool.Add(new Question { A = i, B = i });
+ }
+
+ return pool.OrderBy(_ => _random.Next())
+ .Take(10)
+ .ToList();
+ }
+}
+
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..9728329
--- /dev/null
+++ b/ViewModels/MainViewModel.cs
@@ -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 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 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;
+ }
+}
+
diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..e0e5d71
--- /dev/null
+++ b/ViewModels/MainWindowViewModel.cs
@@ -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;
+ };
+
+ }
+}
diff --git a/ViewModels/QuizViewModel.cs b/ViewModels/QuizViewModel.cs
new file mode 100644
index 0000000..a8dd479
--- /dev/null
+++ b/ViewModels/QuizViewModel.cs
@@ -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 _questions = new();
+ public List 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 SubmitCommand { get; }
+
+ private readonly Subject _finished = new();
+ public IObservable 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(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);
+ }
+}
\ No newline at end of file
diff --git a/ViewModels/ResultViewModel.cs b/ViewModels/ResultViewModel.cs
new file mode 100644
index 0000000..b309286
--- /dev/null
+++ b/ViewModels/ResultViewModel.cs
@@ -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 RestartCommand { get; }
+
+ public ResultViewModel()
+ {
+ RestartCommand = ReactiveCommand.Create(
+ execute: () =>
+ {
+ return _model;
+ },
+ canExecute: Observable.Return(true).ObserveOn(RxApp.MainThreadScheduler),
+ outputScheduler: RxApp.MainThreadScheduler);
+ }
+}
+
diff --git a/ViewModels/ViewModelBase.cs b/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..55b2aa4
--- /dev/null
+++ b/ViewModels/ViewModelBase.cs
@@ -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(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (Equals(field, value)) return false;
+ OnPropertyChanging(propertyName);
+ field = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+}
+
+
diff --git a/Views/MainView.axaml b/Views/MainView.axaml
new file mode 100644
index 0000000..7249ff6
--- /dev/null
+++ b/Views/MainView.axaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/MainView.xaml.cs b/Views/MainView.xaml.cs
new file mode 100644
index 0000000..6d026d6
--- /dev/null
+++ b/Views/MainView.xaml.cs
@@ -0,0 +1,39 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using EinmaleinsTrainer.ViewModels;
+
+namespace EinmaleinsTrainer.Views;
+
+public partial class MainView : UserControl
+{
+ public MainView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void Row_Checked(object sender, RoutedEventArgs e)
+ {
+ if (sender is CheckBox cb
+ && DataContext is MainViewModel vm
+ && int.TryParse(cb.Content?.ToString(), out int value))
+ {
+ vm.ToggleRow(value, true);
+ }
+ }
+
+ private void Row_Unchecked(object sender, RoutedEventArgs e)
+ {
+ if (sender is CheckBox cb
+ && DataContext is MainViewModel vm
+ && int.TryParse(cb.Content?.ToString(), out int value))
+ {
+ vm.ToggleRow(value, false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml
new file mode 100644
index 0000000..6228d3e
--- /dev/null
+++ b/Views/MainWindow.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000..1212a63
--- /dev/null
+++ b/Views/MainWindow.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using EinmaleinsTrainer.ViewModels;
+
+namespace EinmaleinsTrainer.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ DataContext = new MainWindowViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Views/QuizView.axaml b/Views/QuizView.axaml
new file mode 100644
index 0000000..47adf03
--- /dev/null
+++ b/Views/QuizView.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/QuizView.xaml.cs b/Views/QuizView.xaml.cs
new file mode 100644
index 0000000..8e11794
--- /dev/null
+++ b/Views/QuizView.xaml.cs
@@ -0,0 +1,26 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+
+namespace EinmaleinsTrainer.Views;
+
+public partial class QuizView : UserControl
+{
+ public QuizView()
+ {
+ InitializeComponent();
+
+ this.AttachedToVisualTree += async (_,_) =>
+ {
+ await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
+
+ var tb = this.FindControl("AnswerTextBox");
+ tb?.Focus();
+ };
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/Views/ResultView.axaml b/Views/ResultView.axaml
new file mode 100644
index 0000000..c3cd464
--- /dev/null
+++ b/Views/ResultView.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/ResultView.xaml.cs b/Views/ResultView.xaml.cs
new file mode 100644
index 0000000..75ecd01
--- /dev/null
+++ b/Views/ResultView.xaml.cs
@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace EinmaleinsTrainer.Views;
+
+public partial class ResultView : UserControl
+{
+ public ResultView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
+
diff --git a/app.manifest b/app.manifest
new file mode 100644
index 0000000..e97b269
--- /dev/null
+++ b/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+