Added first code.
Signed-off-by: Naoriel Sa' Rocí <naoriel@sa-roci.de>
This commit is contained in:
parent
bbf68e1621
commit
4a4fb54143
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,3 +12,5 @@
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
bin/*
|
||||
obj/*
|
||||
|
||||
14
App.axaml
Normal file
14
App.axaml
Normal file
@ -0,0 +1,14 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="EinmaleinsTrainer.App"
|
||||
xmlns:local="using:EinmaleinsTrainer"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.DataTemplates>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
28
App.axaml.cs
Normal file
28
App.axaml.cs
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
BIN
Assets/avalonia-logo.ico
Normal file
BIN
Assets/avalonia-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
28
EinmaleinsTrainer.csproj
Normal file
28
EinmaleinsTrainer.csproj
Normal file
@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\" />
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
24
EinmaleinsTrainer.sln
Normal file
24
EinmaleinsTrainer.sln
Normal file
@ -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
|
||||
35
Models/Question.cs
Normal file
35
Models/Question.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace EinmaleinsTrainer.Models;
|
||||
|
||||
public class Question : IEquatable<Question>
|
||||
{
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
10
Models/QuizModel.cs
Normal file
10
Models/QuizModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EinmaleinsTrainer.Models;
|
||||
|
||||
public class QuizModel
|
||||
{
|
||||
public List<Question> Questions { get; set; } = [];
|
||||
public bool UseTimer { get; set; }
|
||||
public int Seconds { get; set; } = 10;
|
||||
}
|
||||
9
Models/ResultModel.cs
Normal file
9
Models/ResultModel.cs
Normal file
@ -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;
|
||||
}
|
||||
23
Program.cs
Normal file
23
Program.cs
Normal file
@ -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<App>()
|
||||
.UsePlatformDetect()
|
||||
.UseReactiveUI()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
33
Services/QuestionGenerator.cs
Normal file
33
Services/QuestionGenerator.cs
Normal file
@ -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<Question> Generate(List<int> rows, bool includeSquares)
|
||||
{
|
||||
var pool = new List<Question>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
139
ViewModels/MainViewModel.cs
Normal file
139
ViewModels/MainViewModel.cs
Normal 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
ViewModels/MainWindowViewModel.cs
Normal file
73
ViewModels/MainWindowViewModel.cs
Normal 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
ViewModels/QuizViewModel.cs
Normal file
293
ViewModels/QuizViewModel.cs
Normal 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
ViewModels/ResultViewModel.cs
Normal file
64
ViewModels/ResultViewModel.cs
Normal 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
ViewModels/ViewModelBase.cs
Normal file
42
ViewModels/ViewModelBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
Views/MainView.axaml
Normal file
52
Views/MainView.axaml
Normal file
@ -0,0 +1,52 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
|
||||
x:Class="EinmaleinsTrainer.Views.MainView"
|
||||
x:DataType="vm:MainViewModel">
|
||||
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10" Margin="20">
|
||||
|
||||
<TextBlock FontSize="32" Text="Einmaleins Trainer"/>
|
||||
|
||||
<WrapPanel>
|
||||
<CheckBox FontSize="24" Content="1" IsChecked="{Binding IsRow1Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="2" IsChecked="{Binding IsRow2Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="3" IsChecked="{Binding IsRow3Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="4" IsChecked="{Binding IsRow4Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="5" IsChecked="{Binding IsRow5Checked}" Width="60" />
|
||||
</WrapPanel>
|
||||
|
||||
<WrapPanel>
|
||||
<CheckBox FontSize="24" Content="6" IsChecked="{Binding IsRow6Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="7" IsChecked="{Binding IsRow7Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="8" IsChecked="{Binding IsRow8Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="9" IsChecked="{Binding IsRow9Checked}" Width="60" />
|
||||
<CheckBox FontSize="24" Content="10" IsChecked="{Binding IsRow10Checked}" Width="60" />
|
||||
</WrapPanel>
|
||||
|
||||
<CheckBox FontSize="24" Content="Quadratzahlen" IsChecked="{Binding IncludeSquares}" />
|
||||
|
||||
<WrapPanel>
|
||||
<Grid Height="44">
|
||||
<CheckBox FontSize="24" Content="Zeitlimit" VerticalAlignment="Center"
|
||||
IsChecked="{Binding UseTimer}" Width="140"/>
|
||||
</Grid>
|
||||
|
||||
<NumericUpDown Minimum="2" Maximum="120" FontSize="24" Value="{Binding SecondsPerQuestion, Mode=TwoWay}" Width="140" FormatString="F0" ParsingNumberStyle="Integer" IsVisible="{Binding UseTimer}" />
|
||||
|
||||
<Grid Height="40">
|
||||
<TextBlock FontSize="24" Text=" s" VerticalAlignment="Center" IsVisible="{Binding UseTimer}" />
|
||||
</Grid>
|
||||
</WrapPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Center" >
|
||||
<Button FontSize="24" Content="Start" IsDefault="True"
|
||||
Command="{Binding StartCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
39
Views/MainView.xaml.cs
Normal file
39
Views/MainView.xaml.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Views/MainWindow.axaml
Normal file
25
Views/MainWindow.axaml
Normal file
@ -0,0 +1,25 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
|
||||
xmlns:views="using:EinmaleinsTrainer.Views"
|
||||
x:Class="EinmaleinsTrainer.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<Window.DataTemplates>
|
||||
<DataTemplate DataType="vm:MainViewModel">
|
||||
<views:MainView/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="vm:QuizViewModel">
|
||||
<views:QuizView/>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="vm:ResultViewModel">
|
||||
<views:ResultView/>
|
||||
</DataTemplate>
|
||||
</Window.DataTemplates>
|
||||
|
||||
<ContentControl Content="{Binding Current}" />
|
||||
|
||||
</Window>
|
||||
|
||||
19
Views/MainWindow.xaml.cs
Normal file
19
Views/MainWindow.xaml.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
29
Views/QuizView.axaml
Normal file
29
Views/QuizView.axaml
Normal file
@ -0,0 +1,29 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
|
||||
x:Class="EinmaleinsTrainer.Views.QuizView"
|
||||
x:DataType="vm:QuizViewModel">
|
||||
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="12" Margin="20">
|
||||
|
||||
<TextBlock FontSize="18"
|
||||
Text="{Binding QuestionNumber, StringFormat='Frage {0}'}" />
|
||||
|
||||
<TextBlock FontSize="36"
|
||||
Text="{Binding CurrentQuestionText}" />
|
||||
|
||||
<TextBox x:Name="AnswerTextBox" FontSize="24" Text="{Binding UserAnswer, Mode=TwoWay}" />
|
||||
|
||||
<Button FontSize="24" Content="OK" IsDefault="True"
|
||||
Command="{Binding SubmitCommand}" />
|
||||
|
||||
<TextBlock FontSize="12" Text="{Binding Score, StringFormat='Punkte: {0}'}" />
|
||||
|
||||
<TextBlock FontSize="18" Text="{Binding RemainingSeconds, StringFormat='Zeit: {0} s'}"
|
||||
IsVisible="{Binding UseTimer}" />
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
26
Views/QuizView.xaml.cs
Normal file
26
Views/QuizView.xaml.cs
Normal file
@ -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<TextBox>("AnswerTextBox");
|
||||
tb?.Focus();
|
||||
};
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
29
Views/ResultView.axaml
Normal file
29
Views/ResultView.axaml
Normal file
@ -0,0 +1,29 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
|
||||
x:Class="EinmaleinsTrainer.Views.ResultView"
|
||||
x:DataType="vm:ResultViewModel">
|
||||
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="12" Margin="20">
|
||||
|
||||
<StackPanel HorizontalAlignment="Center" >
|
||||
<TextBlock FontSize="36"
|
||||
Text="{Binding Text}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Center" IsVisible="{Binding ShowTimeRequired}" >
|
||||
<TextBlock FontSize="24"
|
||||
Text="{Binding TimeRequiredText}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Center" >
|
||||
<Button FontSize="36" Content="Neu starten" IsDefault="True"
|
||||
Command="{Binding RestartCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
18
Views/ResultView.xaml.cs
Normal file
18
Views/ResultView.xaml.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
18
app.manifest
Normal file
18
app.manifest
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embedded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="SaRociSolutions.OSS.EinmaleinsTrainer"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Loading…
x
Reference in New Issue
Block a user