diff --git a/10.0/Apps/Procrastinate/README.md b/10.0/Apps/Procrastinate/README.md
new file mode 100644
index 000000000..8be25bf1f
--- /dev/null
+++ b/10.0/Apps/Procrastinate/README.md
@@ -0,0 +1,69 @@
+---
+name: .NET MAUI - Procrastinate
+description: A fun anti-productivity app demonstrating Shell navigation, AI integration (Groq Cloud & Apple Intelligence), localization, theming, and mini-games.
+page_type: sample
+languages:
+- csharp
+- xaml
+products:
+- dotnet-maui
+urlFragment: apps-procrastinate
+---
+
+# Procrastinate - The Ultimate Anti-Productivity App
+
+This sample app demonstrates advanced .NET MAUI features through a humorous anti-productivity theme. It showcases Shell navigation with tabs, AI integration with both cloud (Groq) and on-device (Apple Intelligence) providers, multi-language localization, theming with the Nord color palette, and various mini-games.
+
+
+
+## Features Demonstrated
+
+- **Shell Navigation** - Tab-based navigation with 4 main pages
+- **AI Integration** - Cloud AI (Groq/Llama) and On-Device AI (Apple Intelligence via native xcframework)
+- **Localization** - Full translation in 6 languages (English, French, Spanish, Portuguese, Dutch, Czech)
+- **Custom Theming** - Nord color palette with dark/light theme support
+- **Mini-Games** - 9 games including Simon Says, Minesweeper, Tic Tac Toe (with AI opponent), Snake, Memory Match
+- **Native Interop** - Custom Swift xcframework for Apple Intelligence Foundation Models
+- **XAML Source Generator** - Uses `MauiXamlInflator=SourceGen` for faster XAML loading
+- **Plugin.Maui.Audio** - Sound effects for Simon Says game
+- **Preferences API** - Settings persistence
+- **Responsive UI** - Adapts to different screen sizes
+
+## Project Structure
+
+- `Pages/` - Main pages (Tasks, Games, Excuses, Stats, Settings)
+- `Pages/Games/` - Mini-game implementations
+- `Services/` - AI generators, stats tracking, click tracking
+- `Resources/Strings/` - Localization resource files
+- `Platforms/iOS/FMWrapper.xcframework/` - Native Apple Intelligence wrapper
+
+## AI Setup (Optional)
+
+### Cloud AI (Groq)
+1. Create a free account at [Groq Console](https://console.groq.com/)
+2. Generate an API key
+3. Enter the key in the app's Settings page
+
+### On-Device AI (Apple Intelligence)
+- Requires iOS 18.4+ or macOS 15.4+ with Apple Intelligence enabled
+- Uses the native FMWrapper.xcframework included in the project
+
+## Building
+
+```bash
+# iOS
+dotnet build -f net10.0-ios
+
+# Android
+dotnet build -f net10.0-android
+
+# macOS
+dotnet build -f net10.0-maccatalyst
+```
+
+## Documentation Links
+
+- [Shell Navigation](https://docs.microsoft.com/dotnet/maui/fundamentals/shell/navigation)
+- [Localization](https://docs.microsoft.com/dotnet/maui/fundamentals/localization)
+- [Preferences](https://docs.microsoft.com/dotnet/maui/platform-integration/storage/preferences)
+- [Native Library References](https://docs.microsoft.com/dotnet/maui/platform-integration/native-embedding)
diff --git a/10.0/Apps/Procrastinate/images/screenshots.png b/10.0/Apps/Procrastinate/images/screenshots.png
new file mode 100644
index 000000000..557b5d9c2
Binary files /dev/null and b/10.0/Apps/Procrastinate/images/screenshots.png differ
diff --git a/10.0/Apps/Procrastinate/src/App.xaml b/10.0/Apps/Procrastinate/src/App.xaml
new file mode 100644
index 000000000..570fd8146
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/App.xaml.cs b/10.0/Apps/Procrastinate/src/App.xaml.cs
new file mode 100644
index 000000000..93d888711
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/App.xaml.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace procrastinate;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ // Apply saved theme preference
+ var isLightTheme = Preferences.Get("HighContrastMode", false);
+ UserAppTheme = isLightTheme ? AppTheme.Light : AppTheme.Dark;
+ }
+
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ return new Window(new AppShell());
+ }
+
+ protected override async void OnAppLinkRequestReceived(Uri uri)
+ {
+ base.OnAppLinkRequestReceived(uri);
+
+ // Handle deep links like procrastinate://ExcusePage
+ if (uri.Host is string route && !string.IsNullOrEmpty(route))
+ {
+ await Shell.Current.GoToAsync($"//{route}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/AppShell.xaml b/10.0/Apps/Procrastinate/src/AppShell.xaml
new file mode 100644
index 000000000..12a6ae371
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/AppShell.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/AppShell.xaml.cs b/10.0/Apps/Procrastinate/src/AppShell.xaml.cs
new file mode 100644
index 000000000..6c3a08158
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/AppShell.xaml.cs
@@ -0,0 +1,77 @@
+using procrastinate.Pages;
+using procrastinate.Services;
+
+namespace procrastinate;
+
+public partial class AppShell : Shell
+{
+ private string? _previousTab;
+
+ public AppShell()
+ {
+ InitializeComponent();
+ Routing.RegisterRoute(nameof(SettingsPage), typeof(SettingsPage));
+
+ // Configure platform-specific navigation
+ ConfigurePlatformNavigation();
+
+ // Close settings page when tab changes
+ Navigated += OnShellNavigated;
+ }
+
+ private void ConfigurePlatformNavigation()
+ {
+#if MACCATALYST
+ // Mac Catalyst: Use Flyout sidebar, hide TabBar
+ MobileTabBar.IsVisible = false;
+ DesktopFlyout.IsVisible = true;
+ FlyoutBehavior = FlyoutBehavior.Flyout;
+#else
+ // iOS/Android: Use bottom TabBar, hide Flyout
+ MobileTabBar.IsVisible = true;
+ DesktopFlyout.IsVisible = false;
+ FlyoutBehavior = FlyoutBehavior.Disabled;
+#endif
+ }
+
+ private async void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
+ {
+ var currentLocation = e.Current?.Location?.OriginalString;
+
+ // Only handle root tab navigation (starts with //)
+ if (currentLocation?.StartsWith("//") != true)
+ {
+ return;
+ }
+
+ // Extract the tab name (e.g., "//TasksPage" -> "TasksPage")
+ var currentTab = currentLocation.TrimStart('/').Split('/')[0];
+
+ // Refresh strings to recompute zalgo randomness on navigation
+ AppStrings.Refresh();
+
+ // If we're on the same tab but navigated (e.g., pushed settings then came back),
+ // don't pop - we might have just opened settings
+ if (_previousTab == currentTab)
+ {
+ _previousTab = currentTab;
+ return;
+ }
+
+ // Tab changed - pop any pages on the navigation stack
+ _previousTab = currentTab;
+
+ try
+ {
+ var nav = Current?.CurrentPage?.Navigation;
+ if (nav?.NavigationStack?.Count > 1)
+ {
+ await nav.PopToRootAsync(false);
+ }
+ }
+ catch
+ {
+ // Ignore navigation errors
+ }
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/LICENSE b/10.0/Apps/Procrastinate/src/LICENSE
new file mode 100644
index 000000000..b4abd8086
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Stephane Delcroix
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/10.0/Apps/Procrastinate/src/MauiProgram.cs b/10.0/Apps/Procrastinate/src/MauiProgram.cs
new file mode 100644
index 000000000..6ee8bdbd2
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/MauiProgram.cs
@@ -0,0 +1,69 @@
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Plugin.Maui.Audio;
+using procrastinate.Pages;
+using procrastinate.Services;
+
+namespace procrastinate;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ fonts.AddFont("FontAwesome-Solid.otf", "FontAwesomeSolid");
+ });
+
+ // Register Apple Intelligence chat client and NLEmbedding (iOS/macOS only)
+#if IOS || MACCATALYST
+#pragma warning disable CA1416, MAUIAI0001
+ try
+ {
+ var appleClient = new Microsoft.Maui.Essentials.AI.AppleIntelligenceChatClient();
+ builder.Services.AddSingleton(appleClient);
+ System.Diagnostics.Debug.WriteLine("Apple Intelligence chat client registered");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Apple Intelligence not available: {ex.Message}");
+ }
+
+ try
+ {
+ var nlEmbedding = new Microsoft.Maui.Essentials.AI.NLEmbeddingGenerator();
+ builder.Services.AddSingleton>>(nlEmbedding);
+ System.Diagnostics.Debug.WriteLine("NLEmbedding generator registered");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"NLEmbedding not available: {ex.Message}");
+ }
+#pragma warning restore CA1416, MAUIAI0001
+#endif
+
+ // Services
+ builder.Services.AddSingleton(AudioManager.Current);
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+ // Pages
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/NativeLibrary/FMWrapper.swift b/10.0/Apps/Procrastinate/src/NativeLibrary/FMWrapper.swift
new file mode 100644
index 000000000..413170d0d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/NativeLibrary/FMWrapper.swift
@@ -0,0 +1,118 @@
+import Foundation
+import FoundationModels
+
+/// Objective-C compatible wrapper for Apple's Foundation Models framework
+/// Exposes key functionality to .NET MAUI via ObjCRuntime
+@available(iOS 26.0, macOS 26.0, *)
+@objc(FMWrapper)
+public class FMWrapper: NSObject {
+
+ // Shared state for async operation
+ private static var _lastResult: String?
+ private static var _lastError: String?
+ private static var _isGenerating: Bool = false
+
+ @objc public override init() {
+ super.init()
+ }
+
+ /// Check if Foundation Models is available on this device
+ @objc public static func isAvailable() -> Bool {
+ let model = SystemLanguageModel.default
+ switch model.availability {
+ case .available:
+ return true
+ case .unavailable(_):
+ return false
+ @unknown default:
+ return false
+ }
+ }
+
+ /// Get the reason why Foundation Models is unavailable
+ @objc public static func getUnavailabilityReason() -> String {
+ let model = SystemLanguageModel.default
+ switch model.availability {
+ case .available:
+ return ""
+ case .unavailable(let reason):
+ switch reason {
+ case .deviceNotEligible:
+ return "Device not eligible for Apple Intelligence"
+ case .modelNotReady:
+ return "Apple Intelligence model is not ready yet"
+ case .appleIntelligenceNotEnabled:
+ return "Enable Apple Intelligence in Settings"
+ @unknown default:
+ return "Unknown availability issue"
+ }
+ @unknown default:
+ return "Unable to determine availability"
+ }
+ }
+
+ /// Check if generation is in progress
+ @objc public static func isGenerating() -> Bool {
+ return _isGenerating
+ }
+
+ /// Get the last result (nil if not ready or error)
+ @objc public static func getLastResult() -> String? {
+ return _lastResult
+ }
+
+ /// Get the last error (nil if no error)
+ @objc public static func getLastError() -> String? {
+ return _lastError
+ }
+
+ /// Start generating text (async, poll with isGenerating/getLastResult)
+ @objc public static func startGeneration(prompt: String, instructions: String) {
+ guard !_isGenerating else { return }
+ guard isAvailable() else {
+ _lastError = getUnavailabilityReason()
+ return
+ }
+
+ _isGenerating = true
+ _lastResult = nil
+ _lastError = nil
+
+ Task {
+ do {
+ let session = LanguageModelSession(instructions: instructions)
+ let response = try await session.respond(to: prompt)
+ let content = response.content
+ await MainActor.run {
+ _lastResult = content
+ _isGenerating = false
+ }
+ } catch let error as LanguageModelSession.GenerationError {
+ let errorMessage: String
+ switch error {
+ case .exceededContextWindowSize:
+ errorMessage = "Prompt too long"
+ case .guardrailViolation:
+ errorMessage = "Content policy violation"
+ case .unsupportedLanguageOrLocale:
+ errorMessage = "Language not supported"
+ case .rateLimited:
+ errorMessage = "Too many requests, try again"
+ case .refusal:
+ errorMessage = "Request was refused"
+ default:
+ errorMessage = "Generation failed: \(error.localizedDescription)"
+ }
+ await MainActor.run {
+ _lastError = errorMessage
+ _isGenerating = false
+ }
+ } catch {
+ await MainActor.run {
+ _lastError = "Error: \(error.localizedDescription)"
+ _isGenerating = false
+ }
+ }
+ }
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml b/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml
new file mode 100644
index 000000000..7b6b22f84
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml.cs
new file mode 100644
index 000000000..9cfb9f77b
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/ExcusePage.xaml.cs
@@ -0,0 +1,168 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages;
+
+public partial class ExcusePage : ContentPage
+{
+ private readonly StatsService _statsService;
+ private readonly ExcuseService _excuseService;
+ private string _currentExcuse = "";
+
+ public ExcusePage(StatsService statsService, ExcuseService excuseService)
+ {
+ InitializeComponent();
+ _statsService = statsService;
+ _excuseService = excuseService;
+ UpdateCounterLabel();
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ UpdateCounterLabel();
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ _excuseService.OnPipelineStageChanged = null;
+ _excuseService.OnAgentOutput = null;
+ }
+
+ private void UpdateCounterLabel()
+ {
+ CounterLabel.Text = AppStrings.GetString("ExcusesGenerated", _statsService.ExcusesGenerated);
+ }
+
+ private async void OnSettingsClicked(object? sender, EventArgs e)
+ {
+ await Shell.Current.GoToAsync(nameof(SettingsPage));
+ }
+
+ private async void OnGenerateClicked(object? sender, EventArgs e)
+ {
+ // Refresh zalgo randomness on button click
+ AppStrings.Refresh();
+
+ ShareIconBtn.IsVisible = false;
+ GeneratorInfoLabel.IsVisible = false;
+ PipelineStageLabel.IsVisible = false;
+ AgentReasoningBorder.IsVisible = false;
+ AgentReasoningContent.IsVisible = false;
+ AgentOutputStack.Children.Clear();
+
+ // Show loading state
+ GenerateBtn.IsEnabled = false;
+ ExcuseLabel.Text = AppStrings.GetString("Generating");
+
+ // Wire up pipeline stage callback for agent pipeline mode
+ _excuseService.OnPipelineStageChanged = (stage) =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ PipelineStageLabel.Text = stage;
+ PipelineStageLabel.IsVisible = true;
+ });
+ };
+
+ // Wire up agent reasoning output
+ _excuseService.OnAgentOutput = (agentName, output) =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ var card = new VerticalStackLayout { Spacing = 4 };
+ card.Children.Add(new Label
+ {
+ Text = agentName,
+ FontSize = 13,
+ FontAttributes = FontAttributes.Bold,
+ TextColor = Color.FromArgb("#EBCB8B") // Nord13 — yellow, high contrast on both themes
+ });
+ card.Children.Add(new Label
+ {
+ Text = output,
+ FontSize = 13,
+ TextColor = Application.Current?.RequestedTheme == AppTheme.Light
+ ? Color.FromArgb("#2E3440") // Nord0 — dark text on light bg
+ : Color.FromArgb("#ECEFF4"), // Nord6 — bright text on dark bg
+ LineBreakMode = LineBreakMode.WordWrap
+ });
+ AgentOutputStack.Children.Add(card);
+ });
+ };
+
+ try
+ {
+ var result = await _excuseService.GenerateExcuseAsync(AppStrings.EffectiveLanguage);
+ _currentExcuse = result.Excuse;
+
+ // Never apply Zalgo to the excuse itself
+ ExcuseLabel.Text = _currentExcuse;
+
+ _statsService.IncrementExcusesGenerated();
+ UpdateCounterLabel();
+
+ // Show generator info
+ UpdateGeneratorInfo(result);
+
+ // Show reasoning panel if pipeline mode produced agent outputs
+ if (AgentOutputStack.Children.Count > 0)
+ AgentReasoningBorder.IsVisible = true;
+
+ // Show the share button
+ ShareIconBtn.IsVisible = true;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"Excuse generation error: {ex}");
+ ExcuseLabel.Text = "Something went wrong. Tap to try again! 🔄";
+ }
+ finally
+ {
+ GenerateBtn.IsEnabled = true;
+ }
+ }
+
+ private void UpdateGeneratorInfo(ExcuseResult result)
+ {
+ var parts = new List();
+
+ // Show MEAI badge for AI-powered generators
+ if (result.GeneratorName.Contains("AI"))
+ parts.Add("MEAI");
+
+ parts.Add(result.GeneratorName);
+
+ if (result.Model != null)
+ {
+ parts.Add(result.Model);
+ }
+
+ parts.Add($"{result.Duration.TotalMilliseconds:F0}ms");
+
+ if (result.TokenCount.HasValue)
+ {
+ parts.Add($"{result.TokenCount} tokens");
+ }
+
+ GeneratorInfoLabel.Text = string.Join(" · ", parts);
+ GeneratorInfoLabel.IsVisible = true;
+ }
+
+ private async void OnShareClicked(object? sender, EventArgs e)
+ {
+ if (string.IsNullOrEmpty(_currentExcuse)) return;
+
+ await Share.Default.RequestAsync(new ShareTextRequest
+ {
+ Text = _currentExcuse,
+ Title = AppStrings.GetString("ShareExcuse")
+ });
+ }
+
+ private void OnToggleReasoning(object? sender, EventArgs e)
+ {
+ AgentReasoningContent.IsVisible = !AgentReasoningContent.IsVisible;
+ ReasoningToggleIcon.Text = AgentReasoningContent.IsVisible ? "\uf077" : "\uf078"; // chevron-up / chevron-down
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGame.cs
new file mode 100644
index 000000000..bcea44807
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGame.cs
@@ -0,0 +1,26 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class ClickSpeedGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("ClickSpeed");
+ public override string Icon => "\uf0e7";
+ public override string IconColor => "#D08770";
+ public override string Description => AppStrings.GetString("ClickSpeedDesc");
+
+ private ClickSpeedGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new ClickSpeedGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke(),
+ OnHighScore = score => OnHighScore?.Invoke(Name, score)
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml
new file mode 100644
index 000000000..c9a3c364c
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml.cs
new file mode 100644
index 000000000..b2bf8f8bf
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ClickSpeedGameView.xaml.cs
@@ -0,0 +1,62 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class ClickSpeedGameView : ContentView, INotifyPropertyChanged
+{
+ private int _clickCount;
+ private bool _active;
+ private string _scoreText = "";
+
+ public Action? OnGamePlayed { get; set; }
+ public Action? OnHighScore { get; set; }
+
+ public string ScoreText
+ {
+ get => _scoreText;
+ set { _scoreText = value; OnPropertyChanged(); }
+ }
+
+ public ClickSpeedGameView()
+ {
+ InitializeComponent();
+ BindingContext = this;
+ ScoreText = AppStrings.GetString("Clicks", 0);
+ }
+
+ private async void OnStartClicked(object? sender, EventArgs e)
+ {
+ OnGamePlayed?.Invoke();
+ _clickCount = 0;
+ ScoreText = AppStrings.GetString("Clicks", 0);
+ StartBtn.IsEnabled = false;
+ ClickBtn.IsEnabled = true;
+ _active = true;
+
+ await Task.Delay(5000);
+
+ _active = false;
+ ClickBtn.IsEnabled = false;
+ StartBtn.IsEnabled = true;
+ ScoreText = AppStrings.GetString("FinalClicks", _clickCount, _clickCount / 5.0);
+ OnHighScore?.Invoke(_clickCount);
+ }
+
+ private void OnClickBtnClicked(object? sender, EventArgs e)
+ {
+ if (!_active) return;
+ _clickCount++;
+ ScoreText = AppStrings.GetString("Clicks", _clickCount);
+ }
+
+ public void Stop()
+ {
+ _active = false;
+ }
+
+ public new event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? name = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGame.cs
new file mode 100644
index 000000000..6a8060f32
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGame.cs
@@ -0,0 +1,25 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class MemoryMatchGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("MemoryMatch");
+ public override string Icon => "\uf5fd";
+ public override string IconColor => "#B48EAD";
+ public override string Description => AppStrings.GetString("MemoryMatchDesc");
+
+ private MemoryMatchGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new MemoryMatchGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke()
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml
new file mode 100644
index 000000000..532c10645
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml.cs
new file mode 100644
index 000000000..5ae801452
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MemoryMatchGameView.xaml.cs
@@ -0,0 +1,128 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class MemoryMatchGameView : ContentView
+{
+ private readonly string[] _symbols = ["🐶", "🐱", "🦊", "🐸", "🍎", "🍊", "🍇", "🍓"];
+ private readonly Button[] _cards = new Button[16];
+ private readonly string[] _cardValues = new string[16];
+ private readonly bool[] _matched = new bool[16];
+ private int _firstCard = -1;
+ private int _secondCard = -1;
+ private bool _checking;
+ private int _moves;
+ private int _pairsFound;
+
+ public Action? OnGamePlayed { get; set; }
+
+ public MemoryMatchGameView()
+ {
+ InitializeComponent();
+ CreateCards();
+ ResetGame();
+ }
+
+ private void CreateCards()
+ {
+ for (int i = 0; i < 16; i++)
+ {
+ var idx = i;
+ _cards[i] = new Button
+ {
+ BackgroundColor = Color.FromArgb("#434C5E"),
+ TextColor = Colors.White,
+ FontSize = 24,
+ CornerRadius = 8,
+ Text = "?"
+ };
+ _cards[i].Clicked += (s, e) => OnCardClicked(idx);
+ GameGrid.Add(_cards[i], i % 4, i / 4);
+ }
+ }
+
+ private void ResetGame()
+ {
+ OnGamePlayed?.Invoke();
+ _moves = 0;
+ _pairsFound = 0;
+ _firstCard = -1;
+ _secondCard = -1;
+ _checking = false;
+
+ var values = new List();
+ foreach (var s in _symbols) { values.Add(s); values.Add(s); }
+
+ for (int i = values.Count - 1; i > 0; i--)
+ {
+ int j = Random.Shared.Next(i + 1);
+ (values[i], values[j]) = (values[j], values[i]);
+ }
+
+ for (int i = 0; i < 16; i++)
+ {
+ _cardValues[i] = values[i];
+ _matched[i] = false;
+ _cards[i].Text = "?";
+ _cards[i].BackgroundColor = Color.FromArgb("#434C5E");
+ _cards[i].IsEnabled = true;
+ }
+
+ UpdateStatus();
+ }
+
+ private void OnResetClicked(object? sender, EventArgs e) => ResetGame();
+
+ private async void OnCardClicked(int idx)
+ {
+ if (_checking || _matched[idx] || idx == _firstCard) return;
+
+ _cards[idx].Text = _cardValues[idx];
+ _cards[idx].BackgroundColor = Color.FromArgb("#3B4252");
+
+ if (_firstCard == -1)
+ {
+ _firstCard = idx;
+ }
+ else
+ {
+ _secondCard = idx;
+ _moves++;
+ _checking = true;
+ UpdateStatus();
+
+ await Task.Delay(600);
+
+ if (_cardValues[_firstCard] == _cardValues[_secondCard])
+ {
+ _matched[_firstCard] = true;
+ _matched[_secondCard] = true;
+ _cards[_firstCard].BackgroundColor = Color.FromArgb("#88C0D0");
+ _cards[_secondCard].BackgroundColor = Color.FromArgb("#88C0D0");
+ _pairsFound++;
+ UpdateStatus();
+
+ if (_pairsFound == 8)
+ StatusLabel.Text = AppStrings.GetString("WinInMoves", _moves);
+ }
+ else
+ {
+ _cards[_firstCard].Text = "?";
+ _cards[_secondCard].Text = "?";
+ _cards[_firstCard].BackgroundColor = Color.FromArgb("#434C5E");
+ _cards[_secondCard].BackgroundColor = Color.FromArgb("#434C5E");
+ }
+
+ _firstCard = -1;
+ _secondCard = -1;
+ _checking = false;
+ }
+ }
+
+ private void UpdateStatus()
+ {
+ StatusLabel.Text = AppStrings.GetString("MovesAndPairs", _moves, _pairsFound, 8);
+ }
+
+ public void Stop() => _checking = false;
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGame.cs
new file mode 100644
index 000000000..8129b10b6
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGame.cs
@@ -0,0 +1,25 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class MinesweeperGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("Minesweeper");
+ public override string Icon => "\uf1e2";
+ public override string IconColor => "#A3BE8C";
+ public override string Description => AppStrings.GetString("MinesweeperDesc");
+
+ private MinesweeperGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new MinesweeperGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke()
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml
new file mode 100644
index 000000000..72fa9ea82
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml.cs
new file mode 100644
index 000000000..e701ffa03
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MinesweeperGameView.xaml.cs
@@ -0,0 +1,219 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class MinesweeperGameView : ContentView
+{
+ private const int Size = 6;
+ private const int Mines = 5;
+ private readonly Button[,] _cells = new Button[Size, Size];
+ private readonly bool[,] _isMine = new bool[Size, Size];
+ private readonly bool[,] _revealed = new bool[Size, Size];
+ private readonly bool[,] _flagged = new bool[Size, Size];
+ private bool _gameOver;
+ private bool _firstClick = true;
+
+ public Action? OnGamePlayed { get; set; }
+
+ public MinesweeperGameView()
+ {
+ InitializeComponent();
+ CreateGrid();
+ ResetGame();
+ }
+
+ private void CreateGrid()
+ {
+ for (int i = 0; i < Size; i++)
+ {
+ GameGrid.ColumnDefinitions.Add(new ColumnDefinition(45));
+ GameGrid.RowDefinitions.Add(new RowDefinition(45));
+ }
+
+ for (int r = 0; r < Size; r++)
+ {
+ for (int c = 0; c < Size; c++)
+ {
+ var row = r;
+ var col = c;
+
+ // Use a Border with gesture recognizers for better touch handling
+ var btn = new Button
+ {
+ BackgroundColor = Color.FromArgb("#434C5E"),
+ TextColor = Colors.White,
+ FontSize = 16,
+ CornerRadius = 6,
+ Padding = 0
+ };
+
+ // Track press time for long-press detection
+ DateTime pressStart = DateTime.MinValue;
+
+ btn.Pressed += (s, e) => pressStart = DateTime.Now;
+ btn.Released += (s, e) =>
+ {
+ if (_gameOver) return;
+ var duration = DateTime.Now - pressStart;
+ if (duration.TotalMilliseconds > 400)
+ {
+ // Long press = flag
+ if (!_revealed[row, col])
+ ToggleFlag(row, col);
+ }
+ else
+ {
+ // Short press = reveal
+ OnCellClicked(row, col);
+ }
+ };
+
+ _cells[r, c] = btn;
+ GameGrid.Add(btn, c, r);
+ }
+ }
+ }
+
+ private void ResetGame()
+ {
+ OnGamePlayed?.Invoke();
+ _gameOver = false;
+ _firstClick = true;
+
+ for (int r = 0; r < Size; r++)
+ {
+ for (int c = 0; c < Size; c++)
+ {
+ _isMine[r, c] = false;
+ _revealed[r, c] = false;
+ _flagged[r, c] = false;
+ _cells[r, c].Text = "";
+ _cells[r, c].BackgroundColor = Color.FromArgb("#434C5E");
+ _cells[r, c].IsEnabled = true;
+ }
+ }
+
+ StatusLabel.Text = AppStrings.GetString("FindSafeCells", Mines);
+ }
+
+ private void OnResetClicked(object? sender, EventArgs e) => ResetGame();
+
+ private void ToggleFlag(int row, int col)
+ {
+ if (_revealed[row, col]) return;
+
+ _flagged[row, col] = !_flagged[row, col];
+ _cells[row, col].Text = _flagged[row, col] ? "🚩" : "";
+ _cells[row, col].BackgroundColor = _flagged[row, col]
+ ? Color.FromArgb("#5E81AC")
+ : Color.FromArgb("#434C5E");
+ }
+
+ private void PlaceMines(int safeRow, int safeCol)
+ {
+ int placed = 0;
+ while (placed < Mines)
+ {
+ int r = Random.Shared.Next(Size);
+ int c = Random.Shared.Next(Size);
+ if (!_isMine[r, c] && !(r == safeRow && c == safeCol))
+ {
+ _isMine[r, c] = true;
+ placed++;
+ }
+ }
+ }
+
+ private void OnCellClicked(int row, int col)
+ {
+ if (_gameOver || _revealed[row, col] || _flagged[row, col]) return;
+
+ if (_firstClick)
+ {
+ PlaceMines(row, col);
+ _firstClick = false;
+ }
+
+ RevealCell(row, col);
+ CheckWin();
+ }
+
+ private void RevealCell(int row, int col)
+ {
+ if (row < 0 || row >= Size || col < 0 || col >= Size) return;
+ if (_revealed[row, col]) return;
+
+ _revealed[row, col] = true;
+
+ if (_isMine[row, col])
+ {
+ _cells[row, col].Text = "*";
+ _cells[row, col].BackgroundColor = Color.FromArgb("#BF616A");
+ GameOver(false);
+ return;
+ }
+
+ int count = CountAdjacentMines(row, col);
+ _cells[row, col].BackgroundColor = Color.FromArgb("#3B4252");
+
+ if (count > 0)
+ {
+ _cells[row, col].Text = count.ToString();
+ _cells[row, col].TextColor = count switch
+ {
+ 1 => Color.FromArgb("#81A1C1"),
+ 2 => Color.FromArgb("#A3BE8C"),
+ 3 => Color.FromArgb("#BF616A"),
+ _ => Color.FromArgb("#D08770")
+ };
+ }
+ else
+ {
+ for (int dr = -1; dr <= 1; dr++)
+ for (int dc = -1; dc <= 1; dc++)
+ if (dr != 0 || dc != 0)
+ RevealCell(row + dr, col + dc);
+ }
+ }
+
+ private int CountAdjacentMines(int row, int col)
+ {
+ int count = 0;
+ for (int dr = -1; dr <= 1; dr++)
+ for (int dc = -1; dc <= 1; dc++)
+ {
+ int r = row + dr, c = col + dc;
+ if (r >= 0 && r < Size && c >= 0 && c < Size && _isMine[r, c])
+ count++;
+ }
+ return count;
+ }
+
+ private void CheckWin()
+ {
+ int unrevealed = 0;
+ for (int r = 0; r < Size; r++)
+ for (int c = 0; c < Size; c++)
+ if (!_revealed[r, c])
+ unrevealed++;
+
+ if (unrevealed == Mines)
+ GameOver(true);
+ }
+
+ private void GameOver(bool won)
+ {
+ _gameOver = true;
+ StatusLabel.Text = won ? AppStrings.GetString("YouWin") : AppStrings.GetString("BoomGameOver");
+
+ for (int r = 0; r < Size; r++)
+ for (int c = 0; c < Size; c++)
+ if (_isMine[r, c] && !_revealed[r, c])
+ {
+ _cells[r, c].Text = "X";
+ _cells[r, c].BackgroundColor = Color.FromArgb("#BF616A");
+ }
+ }
+
+ public void Stop() => _gameOver = true;
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/MiniGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/MiniGame.cs
new file mode 100644
index 000000000..fbd42be6d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/MiniGame.cs
@@ -0,0 +1,26 @@
+using Microsoft.Maui.Controls;
+
+namespace procrastinate.Pages.Games;
+
+public abstract class MiniGame
+{
+ public abstract string Name { get; }
+ public abstract string Icon { get; }
+ public abstract string IconColor { get; }
+ public abstract string Description { get; }
+ public abstract View CreateGameView();
+ public abstract void StartGame();
+ public abstract void StopGame();
+
+ public Action? OnGamePlayed { get; set; }
+ public Action? OnHighScore { get; set; }
+ public Action? OnFavoriteToggled { get; set; }
+
+ public string Id => GetType().Name;
+
+ public bool IsFavorite
+ {
+ get => Preferences.Get($"favorite_{Id}", false);
+ set => Preferences.Set($"favorite_{Id}", value);
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGame.cs
new file mode 100644
index 000000000..fab338e65
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGame.cs
@@ -0,0 +1,25 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class NumberGuessingGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("NumberGuess");
+ public override string Icon => "\uf059";
+ public override string IconColor => "#D08770";
+ public override string Description => AppStrings.GetString("NumberGuessDesc");
+
+ private NumberGuessingGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new NumberGuessingGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke()
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml
new file mode 100644
index 000000000..59aa55871
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml.cs
new file mode 100644
index 000000000..4684f1fe0
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/NumberGuessingGameView.xaml.cs
@@ -0,0 +1,70 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class NumberGuessingGameView : ContentView
+{
+ private int _target;
+ private int _attempts;
+ private bool _gameOver;
+
+ public Action? OnGamePlayed { get; set; }
+
+ public NumberGuessingGameView()
+ {
+ InitializeComponent();
+ ResetGame();
+ }
+
+ private void ResetGame()
+ {
+ OnGamePlayed?.Invoke();
+ _target = Random.Shared.Next(1, 101);
+ _attempts = 0;
+ _gameOver = false;
+ ResultLabel.Text = AppStrings.GetString("ThinkingOfNumber");
+ ResultLabel.TextColor = Color.FromArgb("#D8DEE9");
+ AttemptsLabel.Text = AppStrings.GetString("Attempts", 0);
+ GuessEntry.Text = "";
+ GuessEntry.IsEnabled = true;
+ GuessBtn.IsEnabled = true;
+ }
+
+ private void OnGuessClicked(object? sender, EventArgs e)
+ {
+ if (_gameOver) return;
+ if (!int.TryParse(GuessEntry.Text, out int guess) || guess < 1 || guess > 100)
+ {
+ ResultLabel.Text = AppStrings.GetString("EnterNumber1to100");
+ return;
+ }
+
+ _attempts++;
+ AttemptsLabel.Text = AppStrings.GetString("Attempts", _attempts);
+
+ if (guess == _target)
+ {
+ ResultLabel.Text = AppStrings.GetString("Correct", _target);
+ ResultLabel.TextColor = Color.FromArgb("#A3BE8C");
+ _gameOver = true;
+ GuessEntry.IsEnabled = false;
+ GuessBtn.IsEnabled = false;
+ }
+ else if (guess < _target)
+ {
+ ResultLabel.Text = AppStrings.GetString("TooLow", guess);
+ ResultLabel.TextColor = Color.FromArgb("#81A1C1");
+ }
+ else
+ {
+ ResultLabel.Text = AppStrings.GetString("TooHigh", guess);
+ ResultLabel.TextColor = Color.FromArgb("#BF616A");
+ }
+
+ GuessEntry.Text = "";
+ }
+
+ private void OnNewGameClicked(object? sender, EventArgs e) => ResetGame();
+
+ public void Stop() => _gameOver = true;
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGame.cs
new file mode 100644
index 000000000..34f6419ba
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGame.cs
@@ -0,0 +1,25 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class ReactionTimeGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("ReactionTimeGame");
+ public override string Icon => "\uf192";
+ public override string IconColor => "#BF616A";
+ public override string Description => AppStrings.GetString("ReactionTimeDesc");
+
+ private ReactionTimeGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new ReactionTimeGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke()
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml
new file mode 100644
index 000000000..5e5a438d5
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml.cs
new file mode 100644
index 000000000..b4deddc9c
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/ReactionTimeGameView.xaml.cs
@@ -0,0 +1,74 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class ReactionTimeGameView : ContentView
+{
+ private DateTime _startTime;
+ private bool _waiting, _ready;
+ private CancellationTokenSource? _cts;
+
+ public Action? OnGamePlayed { get; set; }
+
+ public ReactionTimeGameView()
+ {
+ InitializeComponent();
+ }
+
+ private async void OnStartClicked(object? sender, EventArgs e)
+ {
+ OnGamePlayed?.Invoke();
+ _cts?.Cancel();
+ _cts = new CancellationTokenSource();
+
+ StartBtn.IsEnabled = false;
+ ReactionBtn.IsEnabled = true;
+ ReactionBtn.BackgroundColor = Colors.Red;
+ ReactionBtn.Text = AppStrings.GetString("Wait");
+ ResultLabel.Text = AppStrings.GetString("WaitForGreen");
+ _waiting = true;
+ _ready = false;
+
+ var delay = Random.Shared.Next(2000, 5000);
+ try
+ {
+ await Task.Delay(delay, _cts.Token);
+ _waiting = false;
+ _ready = true;
+ _startTime = DateTime.Now;
+ ReactionBtn.BackgroundColor = Colors.Green;
+ ReactionBtn.Text = AppStrings.GetString("TapNow");
+ }
+ catch (TaskCanceledException) { }
+ }
+
+ private void OnReactionClicked(object? sender, EventArgs e)
+ {
+ if (_waiting)
+ {
+ _cts?.Cancel();
+ ResultLabel.Text = AppStrings.GetString("TooEarly");
+ ReactionBtn.BackgroundColor = Colors.Orange;
+ ReactionBtn.Text = AppStrings.GetString("TooSoon");
+ StartBtn.IsEnabled = true;
+ ReactionBtn.IsEnabled = false;
+ }
+ else if (_ready)
+ {
+ var time = (DateTime.Now - _startTime).TotalMilliseconds;
+ ResultLabel.Text = AppStrings.GetString("ReactionTime", (int)time);
+ ReactionBtn.BackgroundColor = Colors.Gray;
+ ReactionBtn.Text = AppStrings.GetString("Done");
+ StartBtn.IsEnabled = true;
+ ReactionBtn.IsEnabled = false;
+ _ready = false;
+ }
+ }
+
+ public void Stop()
+ {
+ _cts?.Cancel();
+ _waiting = false;
+ _ready = false;
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGame.cs
new file mode 100644
index 000000000..07cfe6ef9
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGame.cs
@@ -0,0 +1,26 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class SimonSaysGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("SimonSays");
+ public override string Icon => "\uf111";
+ public override string IconColor => "#BF616A";
+ public override string Description => AppStrings.GetString("SimonSaysDesc");
+
+ private SimonSaysGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new SimonSaysGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke(),
+ OnHighScore = score => OnHighScore?.Invoke(Name, score)
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml
new file mode 100644
index 000000000..716fafea7
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml.cs
new file mode 100644
index 000000000..added6b90
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SimonSaysGameView.xaml.cs
@@ -0,0 +1,137 @@
+using Plugin.Maui.Audio;
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class SimonSaysGameView : ContentView
+{
+ private readonly List _sequence = [];
+ private int _index;
+ private bool _playerTurn;
+ private int _score;
+ private readonly Dictionary _sounds = [];
+
+ public Action? OnGamePlayed { get; set; }
+ public Action? OnHighScore { get; set; }
+
+ public SimonSaysGameView()
+ {
+ InitializeComponent();
+ UpdateScoreLabel();
+ _ = LoadSoundsAsync();
+ }
+
+ private async Task LoadSoundsAsync()
+ {
+ var audioManager = AudioManager.Current;
+ var soundFiles = new[] { "simon_red.wav", "simon_green.wav", "simon_blue.wav", "simon_yellow.wav" };
+
+ for (int i = 0; i < soundFiles.Length; i++)
+ {
+ try
+ {
+ var stream = await FileSystem.OpenAppPackageFileAsync(soundFiles[i]);
+ _sounds[i] = audioManager.CreatePlayer(stream);
+ }
+ catch
+ {
+ _sounds[i] = null;
+ }
+ }
+ }
+
+ private void PlaySound(int color)
+ {
+ if (_sounds.TryGetValue(color, out var player) && player != null)
+ {
+ player.Stop();
+ player.Seek(0);
+ player.Play();
+ }
+ }
+
+ private void UpdateScoreLabel()
+ {
+ ScoreLabel.Text = AppStrings.GetString("Score", _score);
+ }
+
+ private async void OnStartClicked(object? sender, EventArgs e)
+ {
+ OnGamePlayed?.Invoke();
+ _sequence.Clear();
+ _score = 0;
+ UpdateScoreLabel();
+ StartBtn.IsEnabled = false;
+ await AddToSequence();
+ }
+
+ private async Task AddToSequence()
+ {
+ _sequence.Add(Random.Shared.Next(4));
+ await PlaySequence();
+ }
+
+ private async Task PlaySequence()
+ {
+ _playerTurn = false;
+ SetButtonsEnabled(false);
+ await Task.Delay(500);
+
+ foreach (var color in _sequence)
+ {
+ var btn = GetButton(color);
+ var original = btn.BackgroundColor;
+ btn.BackgroundColor = GetBrightColor(color);
+ PlaySound(color);
+ await Task.Delay(400);
+ btn.BackgroundColor = original;
+ await Task.Delay(200);
+ }
+
+ _playerTurn = true;
+ _index = 0;
+ SetButtonsEnabled(true);
+ }
+
+ private void OnRedClicked(object? sender, EventArgs e) => OnColorClicked(0);
+ private void OnGreenClicked(object? sender, EventArgs e) => OnColorClicked(1);
+ private void OnBlueClicked(object? sender, EventArgs e) => OnColorClicked(2);
+ private void OnYellowClicked(object? sender, EventArgs e) => OnColorClicked(3);
+
+ private async void OnColorClicked(int color)
+ {
+ if (!_playerTurn) return;
+
+ var btn = GetButton(color);
+ var original = btn.BackgroundColor;
+ btn.BackgroundColor = GetBrightColor(color);
+ PlaySound(color);
+ await Task.Delay(150);
+ btn.BackgroundColor = original;
+
+ if (color == _sequence[_index])
+ {
+ _index++;
+ if (_index >= _sequence.Count)
+ {
+ _score++;
+ UpdateScoreLabel();
+ await Task.Delay(500);
+ _ = AddToSequence();
+ }
+ }
+ else
+ {
+ ScoreLabel.Text = AppStrings.GetString("GameOver", _score);
+ OnHighScore?.Invoke(_score);
+ StartBtn.IsEnabled = true;
+ SetButtonsEnabled(false);
+ }
+ }
+
+ private Button GetButton(int i) => i switch { 0 => RedBtn, 1 => GreenBtn, 2 => BlueBtn, _ => YellowBtn };
+ private static Color GetBrightColor(int i) => i switch { 0 => Colors.Red, 1 => Colors.Lime, 2 => Colors.Blue, _ => Colors.Yellow };
+ private void SetButtonsEnabled(bool e) { RedBtn.IsEnabled = e; GreenBtn.IsEnabled = e; BlueBtn.IsEnabled = e; YellowBtn.IsEnabled = e; }
+
+ public void Stop() => _playerTurn = false;
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGame.cs
new file mode 100644
index 000000000..0420cb85f
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGame.cs
@@ -0,0 +1,26 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class SnakeGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("TiltSnake");
+ public override string Icon => "\uf7a0";
+ public override string IconColor => "#88C0D0";
+ public override string Description => AppStrings.GetString("TiltSnakeDesc");
+
+ private SnakeGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new SnakeGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke(),
+ OnHighScore = score => OnHighScore?.Invoke(Name, score)
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml
new file mode 100644
index 000000000..a2940b50e
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml.cs
new file mode 100644
index 000000000..593cff0e8
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/SnakeGameView.xaml.cs
@@ -0,0 +1,179 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class SnakeGameView : ContentView
+{
+ private const int GridSize = 10;
+ private const int PreferredCellSize = 28;
+ private const int GridSpacing = 1;
+ private int _cellSize = PreferredCellSize;
+ private readonly List<(int r, int c)> _snake = [];
+ private (int r, int c) _food;
+ private (int dr, int dc) _direction = (0, 1);
+ private bool _running;
+ private int _score;
+ private readonly BoxView[,] _cells = new BoxView[GridSize, GridSize];
+
+ public Action? OnGamePlayed { get; set; }
+ public Action? OnHighScore { get; set; }
+
+ public SnakeGameView()
+ {
+ InitializeComponent();
+ SizeChanged += OnSizeChanged;
+ }
+
+ private void OnSizeChanged(object? sender, EventArgs e)
+ {
+ if (Width > 0 && GameGrid.ColumnDefinitions.Count == 0)
+ {
+ CreateGrid();
+ }
+ }
+
+ private void CreateGrid()
+ {
+ // Calculate available width (use parent width minus padding)
+ var availableWidth = Width > 0 ? Width - 40 : 300; // 40px for padding
+
+ // Calculate cell size: use preferred size unless it doesn't fit
+ var maxCellSize = (availableWidth - (GridSize - 1) * GridSpacing) / GridSize;
+ _cellSize = (int)Math.Min(PreferredCellSize, maxCellSize);
+ _cellSize = Math.Max(_cellSize, 16); // Minimum 16px cells
+
+ for (int i = 0; i < GridSize; i++)
+ {
+ GameGrid.ColumnDefinitions.Add(new ColumnDefinition(_cellSize));
+ GameGrid.RowDefinitions.Add(new RowDefinition(_cellSize));
+ }
+
+ for (int r = 0; r < GridSize; r++)
+ {
+ for (int c = 0; c < GridSize; c++)
+ {
+ _cells[r, c] = new BoxView { BackgroundColor = Color.FromArgb("#2E3440"), CornerRadius = 4 };
+ GameGrid.Add(_cells[r, c], c, r);
+ }
+ }
+ }
+
+ private async void OnStartClicked(object? sender, EventArgs e)
+ {
+ if (_running) return;
+ OnGamePlayed?.Invoke();
+
+ _snake.Clear();
+ _snake.Add((GridSize / 2, GridSize / 2));
+ _direction = (0, 1);
+ _score = 0;
+ ScoreLabel.Text = AppStrings.GetString("Score", 0);
+ _running = true;
+ StartBtn.IsEnabled = false;
+
+ SpawnFood();
+ Accelerometer.ReadingChanged += OnAccelerometerReading;
+
+ try { Accelerometer.Start(SensorSpeed.Game); }
+ catch { StatusLabel.Text = AppStrings.GetString("AccelerometerNotAvailable"); }
+
+ await GameLoop();
+ }
+
+ private void OnAccelerometerReading(object? sender, AccelerometerChangedEventArgs e)
+ {
+ var data = e.Reading;
+ if (Math.Abs(data.Acceleration.X) > Math.Abs(data.Acceleration.Y))
+ {
+ _direction = data.Acceleration.X > 0.3 ? (0, -1) : data.Acceleration.X < -0.3 ? (0, 1) : _direction;
+ }
+ else
+ {
+ _direction = data.Acceleration.Y > 0.3 ? (1, 0) : data.Acceleration.Y < -0.3 ? (-1, 0) : _direction;
+ }
+ }
+
+ private async Task GameLoop()
+ {
+ while (_running)
+ {
+ await Task.Delay(200);
+ if (!_running) break;
+
+ var head = _snake[0];
+ var newHead = (r: head.r + _direction.dr, c: head.c + _direction.dc);
+
+ // Wrap around walls instead of dying
+ if (newHead.r < 0) newHead.r = GridSize - 1;
+ else if (newHead.r >= GridSize) newHead.r = 0;
+ if (newHead.c < 0) newHead.c = GridSize - 1;
+ else if (newHead.c >= GridSize) newHead.c = 0;
+
+ if (_snake.Contains(newHead))
+ {
+ GameOver();
+ return;
+ }
+
+ _snake.Insert(0, newHead);
+
+ if (newHead == _food)
+ {
+ _score++;
+ ScoreLabel.Text = AppStrings.GetString("Score", _score);
+ SpawnFood();
+ }
+ else
+ {
+ _snake.RemoveAt(_snake.Count - 1);
+ }
+
+ UpdateGrid();
+ }
+ }
+
+ private void SpawnFood()
+ {
+ var empty = new List<(int r, int c)>();
+ for (int r = 0; r < GridSize; r++)
+ for (int c = 0; c < GridSize; c++)
+ if (!_snake.Contains((r, c)))
+ empty.Add((r, c));
+
+ if (empty.Count > 0)
+ _food = empty[Random.Shared.Next(empty.Count)];
+ }
+
+ private void UpdateGrid()
+ {
+ for (int r = 0; r < GridSize; r++)
+ {
+ for (int c = 0; c < GridSize; c++)
+ {
+ if (_snake.Contains((r, c)))
+ _cells[r, c].BackgroundColor = _snake[0] == (r, c) ? Color.FromArgb("#88C0D0") : Color.FromArgb("#8FBCBB");
+ else if (_food == (r, c))
+ _cells[r, c].BackgroundColor = Color.FromArgb("#BF616A");
+ else
+ _cells[r, c].BackgroundColor = Color.FromArgb("#2E3440");
+ }
+ }
+ }
+
+ private void GameOver()
+ {
+ _running = false;
+ try { Accelerometer.Stop(); } catch { }
+ Accelerometer.ReadingChanged -= OnAccelerometerReading;
+ StatusLabel.Text = AppStrings.GetString("GameOverScore", _score);
+ OnHighScore?.Invoke(_score);
+ StartBtn.IsEnabled = true;
+ }
+
+ public void Stop()
+ {
+ _running = false;
+ try { Accelerometer.Stop(); } catch { }
+ Accelerometer.ReadingChanged -= OnAccelerometerReading;
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGame.cs
new file mode 100644
index 000000000..b89682c58
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGame.cs
@@ -0,0 +1,28 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class TicTacToeGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("TicTacToe");
+ public override string Icon => "\uf00a";
+ public override string IconColor => "#B48EAD";
+ public override string Description => AppStrings.GetString("TicTacToeDesc");
+
+ private TicTacToeGameView? _gameView;
+
+ public TicTacToeAI? AI { get; set; }
+
+ public override View CreateGameView()
+ {
+ _gameView = new TicTacToeGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke()
+ };
+ if (AI is not null) _gameView.SetAI(AI);
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml
new file mode 100644
index 000000000..dbec895f1
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml.cs
new file mode 100644
index 000000000..592afda84
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/TicTacToeGameView.xaml.cs
@@ -0,0 +1,214 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class TicTacToeGameView : ContentView
+{
+ private readonly Button[] _cells = new Button[9];
+ private readonly string[] _board = new string[9];
+ private bool _playerTurn = true;
+ private bool _gameOver;
+ private int _gamesPlayed;
+ private TicTacToeAI? _ticTacToeAI;
+
+ public Action? OnGamePlayed { get; set; }
+
+ public TicTacToeGameView()
+ {
+ InitializeComponent();
+ _gamesPlayed = Preferences.Get("TicTacToeGamesPlayed", 0);
+ CreateCells();
+ ResetGame();
+ }
+
+ ///
+ /// Set the TicTacToeAI instance (injected from parent page).
+ ///
+ public void SetAI(TicTacToeAI ai) => _ticTacToeAI = ai;
+
+ private void CreateCells()
+ {
+ for (int i = 0; i < 9; i++)
+ {
+ var idx = i;
+ _cells[i] = new Button
+ {
+ BackgroundColor = Color.FromArgb("#434C5E"),
+ TextColor = Colors.White,
+ FontSize = 28,
+ FontAttributes = FontAttributes.Bold,
+ CornerRadius = 8
+ };
+ _cells[i].Clicked += (s, e) => OnCellClicked(idx);
+ GameGrid.Add(_cells[i], i % 3, i / 3);
+ }
+ }
+
+ private void ResetGame()
+ {
+ OnGamePlayed?.Invoke();
+ for (int i = 0; i < 9; i++)
+ {
+ _board[i] = "";
+ _cells[i].Text = "";
+ _cells[i].IsEnabled = true;
+ }
+ _playerTurn = true;
+ _gameOver = false;
+ StatusLabel.Text = AppStrings.GetString("YourTurn");
+ }
+
+ private void OnResetClicked(object? sender, EventArgs e) => ResetGame();
+
+ private async void OnCellClicked(int idx)
+ {
+ if (_gameOver || !string.IsNullOrEmpty(_board[idx])) return;
+
+ _board[idx] = "X";
+ _cells[idx].Text = "X";
+ _cells[idx].TextColor = Color.FromArgb("#88C0D0");
+
+ if (CheckWin("X"))
+ {
+ StatusLabel.Text = AppStrings.GetString("YouWin");
+ _gameOver = true;
+ IncrementGamesPlayed();
+ return;
+ }
+
+ if (IsBoardFull())
+ {
+ StatusLabel.Text = AppStrings.GetString("Draw");
+ _gameOver = true;
+ IncrementGamesPlayed();
+ return;
+ }
+
+ _playerTurn = false;
+ StatusLabel.Text = _ticTacToeAI?.IsAvailable == true
+ ? AppStrings.GetString("AIThinking") + " 🤖"
+ : AppStrings.GetString("AIThinking");
+ AIDebugLabel.Text = "";
+ await Task.Delay(300);
+
+ var aiMove = await GetAIMoveAsync();
+
+ // Show AI debug info
+ AIDebugLabel.Text = _ticTacToeAI?.LastDebugInfo ?? "";
+
+ if (aiMove >= 0)
+ {
+ _board[aiMove] = "O";
+ _cells[aiMove].Text = "O";
+ _cells[aiMove].TextColor = Color.FromArgb("#BF616A");
+
+ if (CheckWin("O"))
+ {
+ StatusLabel.Text = AppStrings.GetString("AIWins");
+ _gameOver = true;
+ IncrementGamesPlayed();
+ return;
+ }
+
+ if (IsBoardFull())
+ {
+ StatusLabel.Text = AppStrings.GetString("Draw");
+ _gameOver = true;
+ IncrementGamesPlayed();
+ return;
+ }
+ }
+
+ _playerTurn = true;
+ StatusLabel.Text = AppStrings.GetString("YourTurn");
+ }
+
+ private int GetAIMove()
+ {
+ var empty = Enumerable.Range(0, 9).Where(i => string.IsNullOrEmpty(_board[i])).ToList();
+ if (empty.Count == 0) return -1;
+
+ // Play randomly for the first 2 games, then use strategy
+ if (_gamesPlayed < 2)
+ {
+ return empty[Random.Shared.Next(empty.Count)];
+ }
+
+ // Strategy: Win if possible
+ var winMove = FindWinningMove("O");
+ if (winMove >= 0) return winMove;
+
+ // Strategy: Block player's winning move
+ var blockMove = FindWinningMove("X");
+ if (blockMove >= 0) return blockMove;
+
+ // Strategy: Take center if available
+ if (string.IsNullOrEmpty(_board[4])) return 4;
+
+ // Strategy: Take a corner
+ int[] corners = { 0, 2, 6, 8 };
+ var availableCorners = corners.Where(c => string.IsNullOrEmpty(_board[c])).ToList();
+ if (availableCorners.Count > 0)
+ return availableCorners[Random.Shared.Next(availableCorners.Count)];
+
+ // Take any available edge
+ int[] edges = { 1, 3, 5, 7 };
+ var availableEdges = edges.Where(e => string.IsNullOrEmpty(_board[e])).ToList();
+ if (availableEdges.Count > 0)
+ return availableEdges[Random.Shared.Next(availableEdges.Count)];
+
+ // Fallback: random
+ return empty[Random.Shared.Next(empty.Count)];
+ }
+
+ private async Task GetAIMoveAsync()
+ {
+ // Try real AI if available
+ if (_ticTacToeAI?.IsAvailable == true)
+ {
+ var aiMove = await _ticTacToeAI.GetMoveAsync(_board);
+ if (aiMove >= 0) return aiMove;
+ }
+
+ // Fall back to built-in strategy
+ return GetAIMove();
+ }
+
+ private int FindWinningMove(string player)
+ {
+ int[,] wins = { {0,1,2}, {3,4,5}, {6,7,8}, {0,3,6}, {1,4,7}, {2,5,8}, {0,4,8}, {2,4,6} };
+
+ for (int i = 0; i < 8; i++)
+ {
+ int[] line = { wins[i,0], wins[i,1], wins[i,2] };
+ int playerCount = line.Count(idx => _board[idx] == player);
+ int emptyCount = line.Count(idx => string.IsNullOrEmpty(_board[idx]));
+
+ // If player has 2 in a row and 1 empty, that empty cell is a winning/blocking move
+ if (playerCount == 2 && emptyCount == 1)
+ {
+ return line.First(idx => string.IsNullOrEmpty(_board[idx]));
+ }
+ }
+ return -1;
+ }
+
+ private void IncrementGamesPlayed()
+ {
+ _gamesPlayed++;
+ Preferences.Set("TicTacToeGamesPlayed", _gamesPlayed);
+ }
+
+ private bool CheckWin(string player)
+ {
+ int[,] wins = { {0,1,2}, {3,4,5}, {6,7,8}, {0,3,6}, {1,4,7}, {2,5,8}, {0,4,8}, {2,4,6} };
+ for (int i = 0; i < 8; i++)
+ if (_board[wins[i,0]] == player && _board[wins[i,1]] == player && _board[wins[i,2]] == player)
+ return true;
+ return false;
+ }
+
+ private bool IsBoardFull() => _board.All(c => !string.IsNullOrEmpty(c));
+
+ public void Stop() => _gameOver = true;
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGame.cs b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGame.cs
new file mode 100644
index 000000000..ecea7c9f4
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGame.cs
@@ -0,0 +1,26 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public class WhackAMoleGame : MiniGame
+{
+ public override string Name => AppStrings.GetString("WhackAMole");
+ public override string Icon => "\uf6d3";
+ public override string IconColor => "#B48EAD";
+ public override string Description => AppStrings.GetString("WhackAMoleDesc");
+
+ private WhackAMoleGameView? _gameView;
+
+ public override View CreateGameView()
+ {
+ _gameView = new WhackAMoleGameView
+ {
+ OnGamePlayed = () => OnGamePlayed?.Invoke(),
+ OnHighScore = score => OnHighScore?.Invoke(Name, score)
+ };
+ return _gameView;
+ }
+
+ public override void StartGame() { }
+ public override void StopGame() => _gameView?.Stop();
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml
new file mode 100644
index 000000000..0eceded8d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml.cs
new file mode 100644
index 000000000..949f8d0f0
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/Games/WhackAMoleGameView.xaml.cs
@@ -0,0 +1,121 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages.Games;
+
+public partial class WhackAMoleGameView : ContentView
+{
+ private const int GridSize = 3;
+ private readonly Button[,] _holes = new Button[GridSize, GridSize];
+ private int _score;
+ private int _misses;
+ private bool _running;
+ private CancellationTokenSource? _cts;
+
+ public Action? OnGamePlayed { get; set; }
+ public Action? OnHighScore { get; set; }
+
+ public WhackAMoleGameView()
+ {
+ InitializeComponent();
+ CreateHoles();
+ }
+
+ private void CreateHoles()
+ {
+ for (int r = 0; r < GridSize; r++)
+ {
+ for (int c = 0; c < GridSize; c++)
+ {
+ var row = r;
+ var col = c;
+ _holes[r, c] = new Button
+ {
+ BackgroundColor = Color.FromArgb("#434C5E"),
+ FontSize = 36,
+ CornerRadius = 40,
+ Text = "🕳️"
+ };
+ _holes[r, c].Clicked += (s, e) => OnHoleClicked(row, col);
+ GameGrid.Add(_holes[r, c], c, r);
+ }
+ }
+ }
+
+ private async void OnStartClicked(object? sender, EventArgs e)
+ {
+ if (_running) return;
+ OnGamePlayed?.Invoke();
+
+ _score = 0;
+ _misses = 0;
+ _running = true;
+ _cts = new CancellationTokenSource();
+ StartBtn.IsEnabled = false;
+ UpdateScore();
+
+ for (int r = 0; r < GridSize; r++)
+ for (int c = 0; c < GridSize; c++)
+ _holes[r, c].Text = "🕳️";
+
+ var endTime = DateTime.Now.AddSeconds(30);
+
+ try
+ {
+ while (DateTime.Now < endTime && !_cts.Token.IsCancellationRequested)
+ {
+ int moleR = Random.Shared.Next(GridSize);
+ int moleC = Random.Shared.Next(GridSize);
+ _holes[moleR, moleC].Text = "🐭";
+
+ var showTime = Random.Shared.Next(600, 1200);
+ await Task.Delay(showTime, _cts.Token);
+
+ if (_holes[moleR, moleC].Text == "🐭")
+ {
+ _holes[moleR, moleC].Text = "🕳️";
+ _misses++;
+ UpdateScore();
+ }
+
+ await Task.Delay(200, _cts.Token);
+ }
+ }
+ catch (TaskCanceledException) { }
+
+ _running = false;
+ StartBtn.IsEnabled = true;
+ ScoreLabel.Text = AppStrings.GetString("GameOverScore", _score);
+ OnHighScore?.Invoke(_score);
+ }
+
+ private void OnHoleClicked(int row, int col)
+ {
+ if (!_running) return;
+
+ if (_holes[row, col].Text == "🐭")
+ {
+ _holes[row, col].Text = "💥";
+ _score++;
+ UpdateScore();
+ Task.Delay(100).ContinueWith(_ =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ if (_holes[row, col].Text == "💥")
+ _holes[row, col].Text = "🕳️";
+ });
+ });
+ }
+ }
+
+ private void UpdateScore()
+ {
+ ScoreLabel.Text = AppStrings.GetString("ScoreAndMisses", _score, _misses);
+ }
+
+ public void Stop()
+ {
+ _cts?.Cancel();
+ _running = false;
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml b/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml
new file mode 100644
index 000000000..bae5562b1
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml.cs
new file mode 100644
index 000000000..8731dee1e
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/GamesPage.xaml.cs
@@ -0,0 +1,187 @@
+using Microsoft.Maui.Controls.Shapes;
+using procrastinate.Pages.Games;
+using procrastinate.Services;
+
+namespace procrastinate.Pages;
+
+public partial class GamesPage : ContentPage
+{
+ private readonly StatsService _statsService;
+ private readonly List _allGames;
+ private readonly List _currentGames = [];
+
+ public GamesPage(StatsService statsService, TicTacToeAI ticTacToeAI)
+ {
+ InitializeComponent();
+ _statsService = statsService;
+
+ _allGames =
+ [
+ new SimonSaysGame(),
+ new ClickSpeedGame(),
+ new ReactionTimeGame(),
+ new TicTacToeGame { AI = ticTacToeAI },
+ new MinesweeperGame(),
+ new SnakeGame(),
+ new MemoryMatchGame(),
+ new NumberGuessingGame(),
+ new WhackAMoleGame()
+ ];
+
+ foreach (var game in _allGames)
+ {
+ game.OnGamePlayed = () => _statsService.IncrementGamesPlayed();
+ game.OnHighScore = (name, score) => _statsService.UpdateHighScore(name, score);
+ }
+
+ UpdateShuffleButton();
+ ShuffleGames();
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ UpdateShuffleButton();
+ }
+
+ private void UpdateShuffleButton()
+ {
+ ShuffleBtn.Text = $"\uf074 {AppStrings.GetString("ShuffleGames")}";
+ }
+
+ private async void OnSettingsClicked(object? sender, EventArgs e)
+ {
+ await Shell.Current.GoToAsync(nameof(SettingsPage));
+ }
+
+ private void OnShuffleClicked(object? sender, EventArgs e)
+ {
+ foreach (var game in _currentGames)
+ game.StopGame();
+
+ ShuffleGames();
+ }
+
+ private void ShuffleGames()
+ {
+ _currentGames.Clear();
+ GamesContainer.Children.Clear();
+
+ var weightedGames = new List();
+ foreach (var game in _allGames)
+ {
+ weightedGames.Add(game);
+ if (game.IsFavorite)
+ {
+ weightedGames.Add(game);
+ weightedGames.Add(game);
+ }
+ }
+
+ var selected = new List();
+ var shuffled = weightedGames.OrderBy(_ => Random.Shared.Next()).ToList();
+ foreach (var game in shuffled)
+ {
+ if (!selected.Contains(game))
+ {
+ selected.Add(game);
+ if (selected.Count >= 3) break;
+ }
+ }
+
+ _currentGames.AddRange(selected);
+
+ foreach (var game in _currentGames)
+ {
+ var card = CreateGameCard(game);
+ GamesContainer.Children.Add(card);
+ }
+ }
+
+ private Border CreateGameCard(MiniGame game)
+ {
+ var iconLabel = new Label
+ {
+ Text = game.Icon,
+ FontFamily = "FontAwesomeSolid",
+ FontSize = 20,
+ TextColor = Color.FromArgb(game.IconColor),
+ VerticalOptions = LayoutOptions.Center
+ };
+
+ var titleLabel = new Label
+ {
+ Text = game.Name,
+ FontSize = 20,
+ FontAttributes = FontAttributes.Bold,
+ TextColor = Color.FromArgb("#ECEFF4"),
+ VerticalOptions = LayoutOptions.Center
+ };
+
+ var favBtn = new Button
+ {
+ Text = game.IsFavorite ? "\uf004" : "\uf08a",
+ FontFamily = "FontAwesomeSolid",
+ FontSize = 18,
+ BackgroundColor = Colors.Transparent,
+ TextColor = game.IsFavorite ? Color.FromArgb("#BF616A") : Color.FromArgb("#4C566A"),
+ WidthRequest = 40,
+ HeightRequest = 40,
+ Padding = 0
+ };
+ favBtn.Clicked += (s, e) =>
+ {
+ game.IsFavorite = !game.IsFavorite;
+ favBtn.Text = game.IsFavorite ? "\uf004" : "\uf08a";
+ favBtn.TextColor = game.IsFavorite ? Color.FromArgb("#BF616A") : Color.FromArgb("#4C566A");
+ };
+
+ var header = new HorizontalStackLayout
+ {
+ Spacing = 10,
+ HorizontalOptions = LayoutOptions.Center,
+ Children = { iconLabel, titleLabel, favBtn }
+ };
+
+ var descLabel = new Label
+ {
+ Text = game.Description,
+ FontSize = 14,
+ TextColor = Color.FromArgb("#D8DEE9"),
+ HorizontalOptions = LayoutOptions.Center
+ };
+
+ var gameView = game.CreateGameView();
+
+ var content = new VerticalStackLayout
+ {
+ Spacing = 16,
+ Children = { header, descLabel, gameView }
+ };
+
+ var border = new Border
+ {
+ BackgroundColor = Color.FromArgb("#3B4252"),
+ StrokeShape = new RoundRectangle { CornerRadius = 16 },
+ Stroke = Colors.Transparent,
+ Padding = 20,
+ Content = content,
+ Shadow = new Shadow
+ {
+ Brush = new SolidColorBrush(Color.FromArgb("#40000000")),
+ Offset = new Point(0, 4),
+ Radius = 12,
+ Opacity = 0.3f
+ }
+ };
+
+ return border;
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ foreach (var game in _currentGames)
+ game.StopGame();
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml b/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml
new file mode 100644
index 000000000..e863b207e
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml
@@ -0,0 +1,366 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml.cs
new file mode 100644
index 000000000..c54e2da7b
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/SettingsPage.xaml.cs
@@ -0,0 +1,290 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages;
+
+public partial class SettingsPage : ContentPage
+{
+ private static readonly string[] GroqModels =
+ {
+ "llama-3.3-70b-versatile",
+ "llama-3.1-8b-instant",
+ "gemma2-9b-it"
+ };
+
+ private readonly ExcuseService _excuseService;
+
+ public SettingsPage(ExcuseService excuseService)
+ {
+ _excuseService = excuseService;
+ InitializeComponent();
+ LoadSettings();
+ }
+
+ private void LoadSettings()
+ {
+ // Load language picker
+ foreach (var lang in AppStrings.SupportedLanguages)
+ LanguagePicker.Items.Add(lang.Value);
+
+ var savedLang = Preferences.Get("AppLanguage", "");
+ var langIndex = AppStrings.SupportedLanguages.Keys.ToList().IndexOf(savedLang);
+ LanguagePicker.SelectedIndex = langIndex >= 0 ? langIndex : 0;
+
+ // Load high contrast
+ var isHighContrast = Preferences.Get("HighContrastMode", false);
+ HighContrastSwitch.IsToggled = isHighContrast;
+ UpdatePreview(isHighContrast);
+ UpdateThemeLabel(isHighContrast);
+
+ // Load Zalgo mode - always show zalgo text for the description
+ ZalgoSwitch.IsToggled = AppStrings.IsZalgoMode;
+ var zalgoDesc = AppStrings.Instance["ZalgoModeDesc"];
+ ZalgoDescLabel.Text = AppStrings.Zalgoify(zalgoDesc);
+
+ // Load excuse engine settings
+ foreach (var mode in ExcuseService.AvailableModes)
+ ExcuseModePicker.Items.Add(mode.Value);
+
+ var currentMode = ExcuseService.CurrentMode;
+ var modeIndex = ExcuseService.AvailableModes.Keys.ToList().IndexOf(currentMode);
+ ExcuseModePicker.SelectedIndex = modeIndex >= 0 ? modeIndex : 0;
+
+ GroqApiKeyEntry.Text = SecureStorage.GetAsync("GroqApiKey").GetAwaiter().GetResult() ?? "";
+
+ // Load Groq models picker
+ foreach (var model in GroqModels)
+ GroqModelPicker.Items.Add(model);
+
+ var savedModel = Preferences.Get("GroqModel", "llama-3.3-70b-versatile");
+ var modelIndex = Array.IndexOf(GroqModels, savedModel);
+ GroqModelPicker.SelectedIndex = modelIndex >= 0 ? modelIndex : 0;
+
+ UpdateAISettingsVisibility();
+
+ // Display version
+ var version = AppInfo.VersionString;
+ var build = AppInfo.BuildString;
+ VersionLabel.Text = $"Procrastinate v{version} (build {build})";
+ }
+
+ private void OnLanguageChanged(object? sender, EventArgs e)
+ {
+ if (LanguagePicker.SelectedIndex < 0) return;
+
+ var langCode = AppStrings.SupportedLanguages.Keys.ElementAt(LanguagePicker.SelectedIndex);
+ AppStrings.CurrentLanguage = langCode;
+ UpdateThemeLabel(Preferences.Get("HighContrastMode", false));
+ }
+
+ private void UpdateThemeLabel(bool isHighContrast)
+ {
+ var themeName = isHighContrast ? AppStrings.GetString("HighContrast") : AppStrings.GetString("DefaultTheme");
+ ThemeLabel.Text = AppStrings.GetString("CurrentTheme", themeName);
+ }
+
+ private void OnHighContrastToggled(object? sender, ToggledEventArgs e)
+ {
+ Preferences.Set("HighContrastMode", e.Value);
+ UpdatePreview(e.Value);
+ // Use AppTheme to switch - AppThemeBinding in XAML handles the rest
+ if (Application.Current != null)
+ Application.Current.UserAppTheme = e.Value ? AppTheme.Light : AppTheme.Dark;
+ UpdateThemeLabel(e.Value);
+ }
+
+ private void OnZalgoToggled(object? sender, ToggledEventArgs e)
+ {
+ AppStrings.IsZalgoMode = e.Value;
+ }
+
+ private void OnExcuseModeChanged(object? sender, EventArgs e)
+ {
+ if (ExcuseModePicker.SelectedIndex < 0) return;
+
+ var modeKey = ExcuseService.AvailableModes.Keys.ElementAt(ExcuseModePicker.SelectedIndex);
+ ExcuseService.CurrentMode = modeKey;
+ UpdateAISettingsVisibility();
+ }
+
+ private void UpdateAISettingsVisibility()
+ {
+ CloudSettingsPanel.IsVisible = ExcuseService.CurrentMode == "cloud";
+ OnDeviceAISettingsPanel.IsVisible = ExcuseService.CurrentMode == "ondevice";
+ PipelineSettingsPanel.IsVisible = ExcuseService.CurrentMode == "pipeline";
+ CustomEndpointPanel.IsVisible = ExcuseService.CurrentMode == "custom";
+ EmbeddedModelPanel.IsVisible = ExcuseService.CurrentMode == "embedded";
+
+ if (ExcuseService.CurrentMode == "ondevice")
+ {
+ UpdateOnDeviceAIStatus();
+ }
+ if (ExcuseService.CurrentMode == "custom")
+ {
+ LoadCustomEndpointSettings();
+ }
+ if (ExcuseService.CurrentMode == "embedded")
+ {
+ UpdateEmbeddedModelStatus();
+ }
+ }
+
+ private void UpdateOnDeviceAIStatus()
+ {
+ if (_excuseService.IsOnDeviceAvailable)
+ {
+ OnDeviceAIStatusLabel.Text = "✅ " + AppStrings.Instance.OnDeviceAIAvailable + " (via MEAI IChatClient)";
+ OnDeviceAIStatusLabel.TextColor = Color.FromArgb("#88C0D0");
+ }
+ else
+ {
+ OnDeviceAIStatusLabel.Text = AppStrings.Instance.OnDeviceAIUnavailable;
+ OnDeviceAIStatusLabel.TextColor = Color.FromArgb("#D08770");
+ }
+ }
+
+ private void OnGroqApiKeyChanged(object? sender, TextChangedEventArgs e)
+ {
+ _ = SecureStorage.SetAsync("GroqApiKey", e.NewTextValue ?? "");
+ }
+
+ private void OnGroqModelChanged(object? sender, EventArgs e)
+ {
+ if (GroqModelPicker.SelectedIndex < 0) return;
+ var model = GroqModels[GroqModelPicker.SelectedIndex];
+ Preferences.Set("GroqModel", model);
+ }
+
+ private void UpdatePreview(bool highContrast)
+ {
+ if (highContrast)
+ {
+ // Nord Light preview
+ PreviewPrimary.BackgroundColor = Color.FromArgb("#5E81AC");
+ PreviewSecondary.BackgroundColor = Color.FromArgb("#81A1C1");
+ PreviewTertiary.BackgroundColor = Color.FromArgb("#88C0D0");
+ PreviewAccent.BackgroundColor = Color.FromArgb("#A3BE8C");
+ }
+ else
+ {
+ // Nord Dark preview
+ PreviewPrimary.BackgroundColor = Color.FromArgb("#88C0D0");
+ PreviewSecondary.BackgroundColor = Color.FromArgb("#81A1C1");
+ PreviewTertiary.BackgroundColor = Color.FromArgb("#5E81AC");
+ PreviewAccent.BackgroundColor = Color.FromArgb("#A3BE8C");
+ }
+ }
+
+ private async void OnGitHubTapped(object? sender, EventArgs e)
+ {
+ try
+ {
+ await Launcher.OpenAsync("https://github.com/StephaneDelcroix/procrastinate");
+ }
+ catch { }
+ }
+
+ private void LoadCustomEndpointSettings()
+ {
+ CustomEndpointEntry.Text = Preferences.Get("CustomAIEndpoint", "");
+ CustomApiKeyEntry.Text = SecureStorage.GetAsync("CustomAIApiKey").GetAwaiter().GetResult() ?? "";
+ CustomModelEntry.Text = Preferences.Get("CustomAIModel", "");
+ }
+
+ private void OnCustomEndpointChanged(object? sender, TextChangedEventArgs e)
+ {
+ Preferences.Set("CustomAIEndpoint", e.NewTextValue ?? "");
+ }
+
+ private void OnCustomApiKeyChanged(object? sender, TextChangedEventArgs e)
+ {
+ _ = SecureStorage.SetAsync("CustomAIApiKey", e.NewTextValue ?? "");
+ }
+
+ private void OnCustomModelChanged(object? sender, TextChangedEventArgs e)
+ {
+ Preferences.Set("CustomAIModel", e.NewTextValue ?? "");
+ }
+
+ // -- Embedded ONNX Model --
+
+ private CancellationTokenSource? _downloadCts;
+
+ private void UpdateEmbeddedModelStatus()
+ {
+ var model = OnnxModelManager.AvailableModels[0];
+ if (OnnxModelManager.IsModelDownloaded(model.Id))
+ {
+ var size = OnnxModelManager.GetDownloadedSize(model.Id);
+ EmbeddedModelStatusLabel.Text = $"✅ {model.Name} — Ready ({size / (1024.0 * 1024 * 1024):F1} GB)";
+ EmbeddedModelStatusLabel.TextColor = Color.FromArgb("#A3BE8C");
+ EmbeddedDownloadBtn.IsVisible = false;
+ EmbeddedDeleteBtn.IsVisible = true;
+ EmbeddedDownloadProgress.IsVisible = false;
+ EmbeddedDownloadDetailLabel.IsVisible = false;
+ }
+ else
+ {
+ EmbeddedModelStatusLabel.Text = $"⬇ {model.Name} — Not downloaded";
+ EmbeddedModelStatusLabel.TextColor = Color.FromArgb("#D08770");
+ EmbeddedDownloadBtn.IsVisible = true;
+ EmbeddedDownloadBtn.Text = $"Download {model.Name}";
+ EmbeddedDeleteBtn.IsVisible = false;
+ }
+ }
+
+ private async void OnDownloadEmbeddedModel(object? sender, EventArgs e)
+ {
+ var model = OnnxModelManager.AvailableModels[0];
+ _downloadCts?.Cancel();
+ _downloadCts = new CancellationTokenSource();
+
+ EmbeddedDownloadBtn.IsEnabled = false;
+ EmbeddedDownloadBtn.Text = "Downloading...";
+ EmbeddedDownloadProgress.IsVisible = true;
+ EmbeddedDownloadProgress.Progress = 0;
+ EmbeddedDownloadDetailLabel.IsVisible = true;
+
+ var progress = new Progress<(long downloaded, long total, string file)>(p =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ var pct = p.total > 0 ? (double)p.downloaded / p.total : 0;
+ EmbeddedDownloadProgress.Progress = pct;
+ var dlMB = p.downloaded / (1024.0 * 1024);
+ var totalMB = p.total / (1024.0 * 1024);
+ EmbeddedDownloadDetailLabel.Text = $"{dlMB:F0} / {totalMB:F0} MB ({pct:P0}) — {p.file}";
+ });
+ });
+
+ try
+ {
+ await OnnxModelManager.DownloadModelAsync(model, progress, _downloadCts.Token);
+ UpdateEmbeddedModelStatus();
+ }
+ catch (OperationCanceledException)
+ {
+ EmbeddedModelStatusLabel.Text = "Download cancelled.";
+ }
+ catch (Exception ex)
+ {
+ EmbeddedModelStatusLabel.Text = $"❌ Download failed: {ex.Message}";
+ EmbeddedDownloadBtn.IsEnabled = true;
+ EmbeddedDownloadBtn.Text = "Retry Download";
+ }
+ }
+
+ private async void OnDeleteEmbeddedModel(object? sender, EventArgs e)
+ {
+ var confirm = await DisplayAlert("Delete Model",
+ "Delete the downloaded ONNX model? This frees ~2.5 GB of storage.",
+ "Delete", "Cancel");
+ if (!confirm) return;
+
+ var model = OnnxModelManager.AvailableModels[0];
+#if !IOS
+ OnnxGenAIChatClient.UnloadCached();
+#endif
+ OnnxModelManager.DeleteModel(model.Id);
+ UpdateEmbeddedModelStatus();
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml b/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml
new file mode 100644
index 000000000..93fad7f26
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml.cs
new file mode 100644
index 000000000..f6b57b8f5
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/StatsPage.xaml.cs
@@ -0,0 +1,161 @@
+using Microsoft.Maui.Controls.Shapes;
+using procrastinate.Services;
+
+namespace procrastinate.Pages;
+
+public partial class StatsPage : ContentPage
+{
+ private readonly StatsService _statsService;
+
+ public StatsPage(StatsService statsService)
+ {
+ InitializeComponent();
+ _statsService = statsService;
+ }
+
+ private async void OnSettingsClicked(object? sender, EventArgs e)
+ {
+ await Shell.Current.GoToAsync(nameof(SettingsPage));
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ RefreshStats();
+ RefreshChart();
+ }
+
+ private void RefreshStats()
+ {
+ TasksAvoidedLabel.Text = _statsService.TasksAvoided.ToString();
+ BreaksTakenLabel.Text = _statsService.BreaksTaken.ToString();
+ ExcusesLabel.Text = _statsService.ExcusesGenerated.ToString();
+ GamesPlayedLabel.Text = _statsService.GamesPlayed.ToString();
+ AICallsLabel.Text = _statsService.AIExcuseCalls.ToString();
+ TotalClicksLabel.Text = _statsService.TotalClicks.ToString();
+
+ RefreshHighScores();
+
+ var totalActivity = _statsService.TasksAvoided + _statsService.BreaksTaken +
+ _statsService.ExcusesGenerated + _statsService.GamesPlayed;
+
+ AchievementLabel.Text = totalActivity switch
+ {
+ 0 => $"{AppStrings.GetString("GettingStarted")} ✅",
+ < 5 => $"{AppStrings.GetString("BeginnerProcrastinator")} 🐣",
+ < 15 => GetRandomAchievement(),
+ _ => $"🌟 {AppStrings.GetString("LegendaryProcrastinator")} 🌟"
+ };
+ }
+
+ private void RefreshChart()
+ {
+ ChartTitleLabel.Text = AppStrings.GetString("Last7Days");
+ ChartGrid.Children.Clear();
+
+ var dailyStats = _statsService.GetDailyStats(7);
+ var maxTotal = dailyStats.Max(d => d.Stats.Total);
+ if (maxTotal == 0) maxTotal = 1;
+
+ for (int i = 0; i < dailyStats.Count; i++)
+ {
+ var (date, stats) = dailyStats[i];
+ var heightPercent = (double)stats.Total / maxTotal;
+
+ var barContainer = new Grid
+ {
+ RowDefinitions = [new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto)]
+ };
+
+ var bar = new Border
+ {
+ BackgroundColor = GetBarColor(i),
+ StrokeShape = new RoundRectangle { CornerRadius = new CornerRadius(4, 4, 0, 0) },
+ Stroke = Colors.Transparent,
+ HeightRequest = Math.Max(4, 80 * heightPercent),
+ VerticalOptions = LayoutOptions.End
+ };
+
+ var dayLabel = new Label
+ {
+ Text = date.ToString("ddd")[..2],
+ FontSize = 10,
+ TextColor = (Color)Application.Current!.Resources["Nord4"],
+ HorizontalOptions = LayoutOptions.Center
+ };
+
+ var countLabel = new Label
+ {
+ Text = stats.Total > 0 ? stats.Total.ToString() : "",
+ FontSize = 9,
+ TextColor = (Color)Application.Current!.Resources["Nord4"],
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.End,
+ Margin = new Thickness(0, 0, 0, 4)
+ };
+
+ barContainer.Add(bar, 0, 0);
+ barContainer.Add(countLabel, 0, 0);
+ barContainer.Add(dayLabel, 0, 1);
+
+ ChartGrid.Add(barContainer, i, 0);
+ }
+ }
+
+ private static Color GetBarColor(int index)
+ {
+ var colors = new[]
+ {
+ Color.FromArgb("#88C0D0"),
+ Color.FromArgb("#D08770"),
+ Color.FromArgb("#B48EAD"),
+ Color.FromArgb("#B48EAD"),
+ Color.FromArgb("#81A1C1"),
+ Color.FromArgb("#A3BE8C"),
+ Color.FromArgb("#D08770")
+ };
+ return colors[index % colors.Length];
+ }
+
+ private void RefreshHighScores()
+ {
+ var highScores = _statsService.GameHighScores;
+
+ if (highScores.Count == 0)
+ {
+ NoHighScoresLabel.IsVisible = true;
+ return;
+ }
+
+ NoHighScoresLabel.IsVisible = false;
+
+ var toRemove = HighScoresStack.Children.Where(c => c != NoHighScoresLabel).ToList();
+ foreach (var child in toRemove)
+ HighScoresStack.Children.Remove(child);
+
+ foreach (var (game, score) in highScores.OrderByDescending(x => x.Value))
+ {
+ var scoreLabel = new Label
+ {
+ Text = $"{game}: {score}",
+ FontSize = 16,
+ TextColor = (Color)Application.Current!.Resources["Nord5"]
+ };
+ HighScoresStack.Children.Add(scoreLabel);
+ }
+ }
+
+ private string GetRandomAchievement()
+ {
+ var achievements = AppStrings.CurrentLanguage switch
+ {
+ "fr" => new[] { "Professionnel de la pause 🛋️", "Maître de demain 📅", "Artiste des excuses 🎨", "Rebelle de la productivité 😎" },
+ "es" => new[] { "Profesional del descanso 🛋️", "Maestro del mañana 📅", "Artista de excusas 🎨", "Rebelde de productividad 😎" },
+ "pt" => new[] { "Profissional da pausa 🛋️", "Mestre do amanhã 📅", "Artista de desculpas 🎨", "Rebelde da produtividade 😎" },
+ "nl" => new[] { "Professionele pauzenemer 🛋️", "Meester van morgen 📅", "Excuuskunstenaar 🎨", "Productiviteitsrebel 😎" },
+ "cs" => new[] { "Profesionální pausař 🛋️", "Mistr zítřka 📅", "Umělec výmluv 🎨", "Rebel produktivity 😎" },
+ _ => new[] { "Professional Break Taker 🛋️", "Master of Tomorrow 📅", "Expert Excuse Artist 🎨", "Productivity Rebel 😎" }
+ };
+ return achievements[Random.Shared.Next(achievements.Length)];
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml b/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml
new file mode 100644
index 000000000..d3a2115df
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml.cs b/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml.cs
new file mode 100644
index 000000000..2342a3c78
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Pages/TasksPage.xaml.cs
@@ -0,0 +1,147 @@
+using procrastinate.Services;
+
+namespace procrastinate.Pages;
+
+public partial class TasksPage : ContentPage
+{
+ private readonly StatsService _statsService;
+
+ public TasksPage(StatsService statsService)
+ {
+ InitializeComponent();
+ _statsService = statsService;
+ }
+
+ private async void OnSettingsClicked(object? sender, EventArgs e)
+ {
+ await Shell.Current.GoToAsync(nameof(SettingsPage));
+ }
+
+ private void OnTaskCardTapped(object? sender, TappedEventArgs e)
+ {
+ TaskCheckBox.IsChecked = !TaskCheckBox.IsChecked;
+ }
+
+ private async void OnTaskChecked(object? sender, CheckedChangedEventArgs e)
+ {
+ if (e.Value)
+ {
+ _statsService.IncrementBreaksTaken();
+ MotivationLabel.Text = AppStrings.GetString("Congratulations");
+ await Task.Delay(2000);
+ TaskCheckBox.IsChecked = false;
+ MotivationLabel.Text = AppStrings.GetString("NeedAnotherBreak");
+ }
+ }
+
+ private async void OnAddTaskClicked(object? sender, EventArgs e)
+ {
+ // Refresh zalgo randomness on button click
+ AppStrings.Refresh();
+
+ _statsService.IncrementTasksAvoided();
+ var excuses = GetLocalizedExcuses();
+ var excuse = excuses[Random.Shared.Next(excuses.Length)];
+
+ // Apply Zalgo if enabled
+ var displayExcuse = AppStrings.IsZalgoMode ? AppStrings.Zalgoify(excuse) : excuse;
+ await DisplayAlertAsync("❌", displayExcuse, "OK");
+ }
+
+ private string[] GetLocalizedExcuses()
+ {
+ return AppStrings.CurrentLanguage switch
+ {
+ "fr" => [
+ "Oups! La liste est pleine. Réessayez demain!",
+ "Erreur 404: Productivité introuvable.",
+ "Tâche rejetée: Vous méritez une pause!",
+ "Le serveur fait la sieste. Comme vous devriez!",
+ "Productivité maximale atteinte! (1 tâche = maximum)",
+ "Votre quota de tâches est épuisé. Revenez l'année prochaine!",
+ "Impossible d'ajouter: Le bonheur est plus important.",
+ "La liste des tâches est en grève. Solidarité!",
+ "Votre cerveau a besoin de repos. C'est scientifique!",
+ "Erreur système: Trop de motivation détectée."
+ ],
+ "es" => [
+ "¡Ups! La lista está llena. ¡Inténtalo mañana!",
+ "Error 404: Productividad no encontrada.",
+ "¡Tarea rechazada: Te mereces un descanso!",
+ "El servidor está durmiendo. ¡Como tú deberías!",
+ "¡Productividad máxima alcanzada! (1 tarea = máximo)",
+ "Tu cuota de tareas está agotada. ¡Vuelve el próximo año!",
+ "Imposible agregar: La felicidad es más importante.",
+ "La lista de tareas está en huelga. ¡Solidaridad!",
+ "Tu cerebro necesita descanso. ¡Es científico!",
+ "Error del sistema: Demasiada motivación detectada."
+ ],
+ "pt" => [
+ "Ops! A lista está cheia. Tente amanhã!",
+ "Erro 404: Produtividade não encontrada.",
+ "Tarefa rejeitada: Você merece uma pausa!",
+ "O servidor está dormindo. Como você deveria!",
+ "Produtividade máxima atingida! (1 tarefa = máximo)",
+ "Sua cota de tarefas acabou. Volte no próximo ano!",
+ "Impossível adicionar: A felicidade é mais importante.",
+ "A lista de tarefas está em greve. Solidariedade!",
+ "Seu cérebro precisa de descanso. É científico!",
+ "Erro do sistema: Muita motivação detectada."
+ ],
+ "nl" => [
+ "Oeps! De lijst is vol. Probeer morgen!",
+ "Fout 404: Productiviteit niet gevonden.",
+ "Taak afgewezen: Je verdient een pauze!",
+ "De server slaapt. Net als jij zou moeten!",
+ "Maximale productiviteit bereikt! (1 taak = maximum)",
+ "Je takenlimiet is bereikt. Kom volgend jaar terug!",
+ "Kan niet toevoegen: Geluk is belangrijker.",
+ "De takenlijst staakt. Solidariteit!",
+ "Je brein heeft rust nodig. Het is wetenschap!",
+ "Systeemfout: Te veel motivatie gedetecteerd."
+ ],
+ "cs" => [
+ "Jejda! Seznam je plný. Zkuste zítra!",
+ "Chyba 404: Produktivita nenalezena.",
+ "Úkol odmítnut: Zasloužíte si pauzu!",
+ "Server spí. Jako byste měli vy!",
+ "Maximální produktivita dosažena! (1 úkol = maximum)",
+ "Vaše kvóta úkolů je vyčerpána. Vraťte se příští rok!",
+ "Nelze přidat: Štěstí je důležitější.",
+ "Seznam úkolů stávkuje. Solidarita!",
+ "Váš mozek potřebuje odpočinek. Je to vědecké!",
+ "Systémová chyba: Detekováno příliš mnoho motivace."
+ ],
+ "uk" => [
+ "Ой! Список повний. Спробуйте завтра!",
+ "Помилка 404: Продуктивність не знайдена.",
+ "Завдання відхилено: Ви заслуговуєте на перерву!",
+ "Сервер дрімає. Як і ви повинні!",
+ "Досягнуто максимальну продуктивність! (1 завдання = максимум)",
+ "Ваша квота завдань вичерпана. Повертайтеся наступного року!",
+ "Неможливо додати: Щастя важливіше.",
+ "Список завдань страйкує. Солідарність!",
+ "Вашому мозку потрібен відпочинок. Це наука!",
+ "Системна помилка: Виявлено занадто багато мотивації.",
+ "Переповнення буфера завдань. Будь ласка, прокрастинуйте.",
+ "Додавання завдань тимчасово вимкнено для вашого блага.",
+ "Фея завдань у відпустці. Спробуйте ніколи!"
+ ],
+ _ => [
+ "Oops! The task list is full. Try again tomorrow!",
+ "Error 404: Productivity not found.",
+ "Task rejected: You deserve a break instead!",
+ "Server is napping. Just like you should be!",
+ "Maximum productivity reached! (1 task = maximum)",
+ "Your task quota is exhausted. Come back next year!",
+ "Cannot add: Happiness is more important.",
+ "The task list is on strike. Solidarity!",
+ "Your brain needs rest. It's science!",
+ "System error: Too much motivation detected.",
+ "Task buffer overflow. Please procrastinate.",
+ "Adding tasks is temporarily disabled for your wellbeing.",
+ "The task fairy is on vacation. Try again never!"
+ ]
+ };
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Android/AndroidManifest.xml b/10.0/Apps/Procrastinate/src/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..bdec9b590
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Android/MainActivity.cs b/10.0/Apps/Procrastinate/src/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..2b5d36801
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace procrastinate;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Android/MainApplication.cs b/10.0/Apps/Procrastinate/src/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..2855bb3e6
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Android/MainApplication.cs
@@ -0,0 +1,15 @@
+using Android.App;
+using Android.Runtime;
+
+namespace procrastinate;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Android/Resources/values/colors.xml b/10.0/Apps/Procrastinate/src/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..5cd160496
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/AppDelegate.cs b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 000000000..f5959229d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace procrastinate;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Entitlements.plist b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 000000000..8e87c0cb0
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Info.plist b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 000000000..cfd1c83e2
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ LSApplicationCategoryType
+ public.app-category.lifestyle
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Program.cs b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 000000000..70a497193
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace procrastinate;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml b/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..de25b64c0
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml.cs b/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..8482bfb42
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,24 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace procrastinate.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Windows/Package.appxmanifest b/10.0/Apps/Procrastinate/src/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..4556f6f3e
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/Windows/app.manifest b/10.0/Apps/Procrastinate/src/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..87d937921
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/Windows/app.manifest
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+ true
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/AppDelegate.cs b/10.0/Apps/Procrastinate/src/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..fc13e0337
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,25 @@
+using Foundation;
+using UIKit;
+
+namespace procrastinate;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ public override bool OpenUrl(UIApplication application, NSUrl url, NSDictionary options)
+ {
+ // Forward deep link to MAUI
+ if (url.Scheme == "procrastinate" && url.Host is string route)
+ {
+ MainThread.BeginInvokeOnMainThread(async () =>
+ {
+ await Task.Delay(500); // Wait for app to be ready
+ await Shell.Current.GoToAsync($"//{route}");
+ });
+ return true;
+ }
+ return base.OpenUrl(application, url, options);
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/Info.plist b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/Info.plist
new file mode 100644
index 000000000..fa9640ce2
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/Info.plist
@@ -0,0 +1,43 @@
+
+
+
+
+ AvailableLibraries
+
+
+ BinaryPath
+ FMWrapper.framework/FMWrapper
+ LibraryIdentifier
+ ios-arm64
+ LibraryPath
+ FMWrapper.framework
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+
+
+ BinaryPath
+ FMWrapper.framework/FMWrapper
+ LibraryIdentifier
+ ios-arm64-simulator
+ LibraryPath
+ FMWrapper.framework
+ SupportedArchitectures
+
+ arm64
+
+ SupportedPlatform
+ ios
+ SupportedPlatformVariant
+ simulator
+
+
+ CFBundlePackageType
+ XFWK
+ XCFrameworkFormatVersion
+ 1.0
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/FMWrapper b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/FMWrapper
new file mode 100755
index 000000000..4d7582438
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/FMWrapper differ
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Headers/FMWrapper.h b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Headers/FMWrapper.h
new file mode 100644
index 000000000..5dfde2eb2
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Headers/FMWrapper.h
@@ -0,0 +1,332 @@
+// Generated by Apple Swift version 6.2.1 effective-5.10 (swiftlang-6.2.1.4.8 clang-1700.4.4.1)
+#ifndef FMWRAPPER_SWIFT_H
+#define FMWRAPPER_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include()
+# include
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#if defined(__OBJC__)
+#include
+#endif
+#if defined(__cplusplus)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+#if defined(__cplusplus)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
+#if defined(__arm64e__) && __has_include()
+# include
+#else
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
+# ifndef __ptrauth_swift_value_witness_function_pointer
+# define __ptrauth_swift_value_witness_function_pointer(x)
+# endif
+# ifndef __ptrauth_swift_class_method_pointer
+# define __ptrauth_swift_class_method_pointer(x)
+# endif
+#pragma clang diagnostic pop
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include()
+# include
+# elif !defined(__cplusplus)
+typedef unsigned char char8_t;
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+# define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+#if !defined(SWIFT_RUNTIME_NAME)
+# if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+# else
+# define SWIFT_RUNTIME_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_COMPILE_NAME)
+# if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+# else
+# define SWIFT_COMPILE_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_METHOD_FAMILY)
+# if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+# else
+# define SWIFT_METHOD_FAMILY(X)
+# endif
+#endif
+#if !defined(SWIFT_NOESCAPE)
+# if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+# else
+# define SWIFT_NOESCAPE
+# endif
+#endif
+#if !defined(SWIFT_RELEASES_ARGUMENT)
+# if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+# else
+# define SWIFT_RELEASES_ARGUMENT
+# endif
+#endif
+#if !defined(SWIFT_WARN_UNUSED_RESULT)
+# if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+# else
+# define SWIFT_WARN_UNUSED_RESULT
+# endif
+#endif
+#if !defined(SWIFT_NORETURN)
+# if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+# else
+# define SWIFT_NORETURN
+# endif
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+# define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if __has_attribute(enum_extensibility)
+# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+# define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if !defined(SWIFT_DEPRECATED_OBJC)
+# if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+# else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+# endif
+#endif
+#if defined(__OBJC__)
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#endif
+#if !defined(SWIFT_EXTERN)
+# if defined(__cplusplus)
+# define SWIFT_EXTERN extern "C"
+# else
+# define SWIFT_EXTERN extern
+# endif
+#endif
+#if !defined(SWIFT_CALL)
+# define SWIFT_CALL __attribute__((swiftcall))
+#endif
+#if !defined(SWIFT_INDIRECT_RESULT)
+# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
+#endif
+#if !defined(SWIFT_CONTEXT)
+# define SWIFT_CONTEXT __attribute__((swift_context))
+#endif
+#if !defined(SWIFT_ERROR_RESULT)
+# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
+#endif
+#if defined(__cplusplus)
+# define SWIFT_NOEXCEPT noexcept
+#else
+# define SWIFT_NOEXCEPT
+#endif
+#if !defined(SWIFT_C_INLINE_THUNK)
+# if __has_attribute(always_inline)
+# if __has_attribute(nodebug)
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
+# else
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
+# endif
+# else
+# define SWIFT_C_INLINE_THUNK inline
+# endif
+#endif
+#if defined(_WIN32)
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
+#endif
+#else
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL
+#endif
+#endif
+#if defined(__OBJC__)
+#if __has_feature(objc_modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+@import ObjectiveC;
+#endif
+
+#endif
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
+#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="FMWrapper",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if defined(__OBJC__)
+
+@class NSString;
+/// Objective-C compatible wrapper for Apple’s Foundation Models framework
+/// Exposes key functionality to .NET MAUI via ObjCRuntime
+SWIFT_CLASS_NAMED("FMWrapper") SWIFT_AVAILABILITY(macos,introduced=26.0) SWIFT_AVAILABILITY(ios,introduced=26.0)
+@interface FMWrapper : NSObject
+- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
+/// Check if Foundation Models is available on this device
++ (BOOL)isAvailable SWIFT_WARN_UNUSED_RESULT;
+/// Get the reason why Foundation Models is unavailable
++ (NSString * _Nonnull)getUnavailabilityReason SWIFT_WARN_UNUSED_RESULT;
+/// Check if generation is in progress
++ (BOOL)isGenerating SWIFT_WARN_UNUSED_RESULT;
+/// Get the last result (nil if not ready or error)
++ (NSString * _Nullable)getLastResult SWIFT_WARN_UNUSED_RESULT;
+/// Get the last error (nil if no error)
++ (NSString * _Nullable)getLastError SWIFT_WARN_UNUSED_RESULT;
+/// Start generating text (async, poll with isGenerating/getLastResult)
++ (void)startGenerationWithPrompt:(NSString * _Nonnull)prompt instructions:(NSString * _Nonnull)instructions;
+@end
+
+#endif
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#if defined(__cplusplus)
+#endif
+#pragma clang diagnostic pop
+#endif
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Info.plist b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Info.plist
new file mode 100644
index 000000000..7ba77a46d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ FMWrapper
+ CFBundleIdentifier
+ com.procrastinate.FMWrapper
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ FMWrapper
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 26.0
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Modules/module.modulemap b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Modules/module.modulemap
new file mode 100644
index 000000000..514c1fe3d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64-simulator/FMWrapper.framework/Modules/module.modulemap
@@ -0,0 +1,5 @@
+framework module FMWrapper {
+ umbrella header "FMWrapper.h"
+ export *
+ module * { export * }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/FMWrapper b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/FMWrapper
new file mode 100755
index 000000000..8ac92253c
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/FMWrapper differ
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Headers/FMWrapper.h b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Headers/FMWrapper.h
new file mode 100644
index 000000000..5dfde2eb2
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Headers/FMWrapper.h
@@ -0,0 +1,332 @@
+// Generated by Apple Swift version 6.2.1 effective-5.10 (swiftlang-6.2.1.4.8 clang-1700.4.4.1)
+#ifndef FMWRAPPER_SWIFT_H
+#define FMWRAPPER_SWIFT_H
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wgcc-compat"
+
+#if !defined(__has_include)
+# define __has_include(x) 0
+#endif
+#if !defined(__has_attribute)
+# define __has_attribute(x) 0
+#endif
+#if !defined(__has_feature)
+# define __has_feature(x) 0
+#endif
+#if !defined(__has_warning)
+# define __has_warning(x) 0
+#endif
+
+#if __has_include()
+# include
+#endif
+
+#pragma clang diagnostic ignored "-Wauto-import"
+#if defined(__OBJC__)
+#include
+#endif
+#if defined(__cplusplus)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#else
+#include
+#include
+#include
+#include
+#endif
+#if defined(__cplusplus)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
+#if defined(__arm64e__) && __has_include()
+# include
+#else
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
+# ifndef __ptrauth_swift_value_witness_function_pointer
+# define __ptrauth_swift_value_witness_function_pointer(x)
+# endif
+# ifndef __ptrauth_swift_class_method_pointer
+# define __ptrauth_swift_class_method_pointer(x)
+# endif
+#pragma clang diagnostic pop
+#endif
+#pragma clang diagnostic pop
+#endif
+
+#if !defined(SWIFT_TYPEDEFS)
+# define SWIFT_TYPEDEFS 1
+# if __has_include()
+# include
+# elif !defined(__cplusplus)
+typedef unsigned char char8_t;
+typedef uint_least16_t char16_t;
+typedef uint_least32_t char32_t;
+# endif
+typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
+typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
+typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
+typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
+typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
+typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
+typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
+typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
+typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
+typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
+typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
+typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
+#endif
+
+#if !defined(SWIFT_PASTE)
+# define SWIFT_PASTE_HELPER(x, y) x##y
+# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
+#endif
+#if !defined(SWIFT_METATYPE)
+# define SWIFT_METATYPE(X) Class
+#endif
+#if !defined(SWIFT_CLASS_PROPERTY)
+# if __has_feature(objc_class_property)
+# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
+# else
+# define SWIFT_CLASS_PROPERTY(...)
+# endif
+#endif
+#if !defined(SWIFT_RUNTIME_NAME)
+# if __has_attribute(objc_runtime_name)
+# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
+# else
+# define SWIFT_RUNTIME_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_COMPILE_NAME)
+# if __has_attribute(swift_name)
+# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
+# else
+# define SWIFT_COMPILE_NAME(X)
+# endif
+#endif
+#if !defined(SWIFT_METHOD_FAMILY)
+# if __has_attribute(objc_method_family)
+# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
+# else
+# define SWIFT_METHOD_FAMILY(X)
+# endif
+#endif
+#if !defined(SWIFT_NOESCAPE)
+# if __has_attribute(noescape)
+# define SWIFT_NOESCAPE __attribute__((noescape))
+# else
+# define SWIFT_NOESCAPE
+# endif
+#endif
+#if !defined(SWIFT_RELEASES_ARGUMENT)
+# if __has_attribute(ns_consumed)
+# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
+# else
+# define SWIFT_RELEASES_ARGUMENT
+# endif
+#endif
+#if !defined(SWIFT_WARN_UNUSED_RESULT)
+# if __has_attribute(warn_unused_result)
+# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+# else
+# define SWIFT_WARN_UNUSED_RESULT
+# endif
+#endif
+#if !defined(SWIFT_NORETURN)
+# if __has_attribute(noreturn)
+# define SWIFT_NORETURN __attribute__((noreturn))
+# else
+# define SWIFT_NORETURN
+# endif
+#endif
+#if !defined(SWIFT_CLASS_EXTRA)
+# define SWIFT_CLASS_EXTRA
+#endif
+#if !defined(SWIFT_PROTOCOL_EXTRA)
+# define SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_ENUM_EXTRA)
+# define SWIFT_ENUM_EXTRA
+#endif
+#if !defined(SWIFT_CLASS)
+# if __has_attribute(objc_subclassing_restricted)
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# else
+# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
+# endif
+#endif
+#if !defined(SWIFT_RESILIENT_CLASS)
+# if __has_attribute(objc_class_stub)
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# else
+# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
+# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
+# endif
+#endif
+#if !defined(SWIFT_PROTOCOL)
+# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
+#endif
+#if !defined(SWIFT_EXTENSION)
+# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
+#endif
+#if !defined(OBJC_DESIGNATED_INITIALIZER)
+# if __has_attribute(objc_designated_initializer)
+# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
+# else
+# define OBJC_DESIGNATED_INITIALIZER
+# endif
+#endif
+#if !defined(SWIFT_ENUM_ATTR)
+# if __has_attribute(enum_extensibility)
+# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
+# else
+# define SWIFT_ENUM_ATTR(_extensibility)
+# endif
+#endif
+#if !defined(SWIFT_ENUM)
+# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# if __has_feature(generalized_swift_name)
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
+# else
+# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
+# endif
+#endif
+#if !defined(SWIFT_UNAVAILABLE)
+# define SWIFT_UNAVAILABLE __attribute__((unavailable))
+#endif
+#if !defined(SWIFT_UNAVAILABLE_MSG)
+# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
+#endif
+#if !defined(SWIFT_AVAILABILITY)
+# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
+#endif
+#if !defined(SWIFT_WEAK_IMPORT)
+# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
+#endif
+#if !defined(SWIFT_DEPRECATED)
+# define SWIFT_DEPRECATED __attribute__((deprecated))
+#endif
+#if !defined(SWIFT_DEPRECATED_MSG)
+# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
+#endif
+#if !defined(SWIFT_DEPRECATED_OBJC)
+# if __has_feature(attribute_diagnose_if_objc)
+# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
+# else
+# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
+# endif
+#endif
+#if defined(__OBJC__)
+#if !defined(IBSegueAction)
+# define IBSegueAction
+#endif
+#endif
+#if !defined(SWIFT_EXTERN)
+# if defined(__cplusplus)
+# define SWIFT_EXTERN extern "C"
+# else
+# define SWIFT_EXTERN extern
+# endif
+#endif
+#if !defined(SWIFT_CALL)
+# define SWIFT_CALL __attribute__((swiftcall))
+#endif
+#if !defined(SWIFT_INDIRECT_RESULT)
+# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
+#endif
+#if !defined(SWIFT_CONTEXT)
+# define SWIFT_CONTEXT __attribute__((swift_context))
+#endif
+#if !defined(SWIFT_ERROR_RESULT)
+# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
+#endif
+#if defined(__cplusplus)
+# define SWIFT_NOEXCEPT noexcept
+#else
+# define SWIFT_NOEXCEPT
+#endif
+#if !defined(SWIFT_C_INLINE_THUNK)
+# if __has_attribute(always_inline)
+# if __has_attribute(nodebug)
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
+# else
+# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
+# endif
+# else
+# define SWIFT_C_INLINE_THUNK inline
+# endif
+#endif
+#if defined(_WIN32)
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
+#endif
+#else
+#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
+# define SWIFT_IMPORT_STDLIB_SYMBOL
+#endif
+#endif
+#if defined(__OBJC__)
+#if __has_feature(objc_modules)
+#if __has_warning("-Watimport-in-framework-header")
+#pragma clang diagnostic ignored "-Watimport-in-framework-header"
+#endif
+@import ObjectiveC;
+#endif
+
+#endif
+#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
+#pragma clang diagnostic ignored "-Wduplicate-method-arg"
+#if __has_warning("-Wpragma-clang-attribute")
+# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
+#endif
+#pragma clang diagnostic ignored "-Wunknown-pragmas"
+#pragma clang diagnostic ignored "-Wnullability"
+#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
+#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
+
+#if __has_attribute(external_source_symbol)
+# pragma push_macro("any")
+# undef any
+# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="FMWrapper",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
+# pragma pop_macro("any")
+#endif
+
+#if defined(__OBJC__)
+
+@class NSString;
+/// Objective-C compatible wrapper for Apple’s Foundation Models framework
+/// Exposes key functionality to .NET MAUI via ObjCRuntime
+SWIFT_CLASS_NAMED("FMWrapper") SWIFT_AVAILABILITY(macos,introduced=26.0) SWIFT_AVAILABILITY(ios,introduced=26.0)
+@interface FMWrapper : NSObject
+- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
+/// Check if Foundation Models is available on this device
++ (BOOL)isAvailable SWIFT_WARN_UNUSED_RESULT;
+/// Get the reason why Foundation Models is unavailable
++ (NSString * _Nonnull)getUnavailabilityReason SWIFT_WARN_UNUSED_RESULT;
+/// Check if generation is in progress
++ (BOOL)isGenerating SWIFT_WARN_UNUSED_RESULT;
+/// Get the last result (nil if not ready or error)
++ (NSString * _Nullable)getLastResult SWIFT_WARN_UNUSED_RESULT;
+/// Get the last error (nil if no error)
++ (NSString * _Nullable)getLastError SWIFT_WARN_UNUSED_RESULT;
+/// Start generating text (async, poll with isGenerating/getLastResult)
++ (void)startGenerationWithPrompt:(NSString * _Nonnull)prompt instructions:(NSString * _Nonnull)instructions;
+@end
+
+#endif
+#if __has_attribute(external_source_symbol)
+# pragma clang attribute pop
+#endif
+#if defined(__cplusplus)
+#endif
+#pragma clang diagnostic pop
+#endif
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Info.plist b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Info.plist
new file mode 100644
index 000000000..7ba77a46d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ FMWrapper
+ CFBundleIdentifier
+ com.procrastinate.FMWrapper
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ FMWrapper
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 26.0
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Modules/module.modulemap b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Modules/module.modulemap
new file mode 100644
index 000000000..514c1fe3d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/FMWrapper.xcframework/ios-arm64/FMWrapper.framework/Modules/module.modulemap
@@ -0,0 +1,5 @@
+framework module FMWrapper {
+ umbrella header "FMWrapper.h"
+ export *
+ module * { export * }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/Info.plist b/10.0/Apps/Procrastinate/src/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..a70d3d0dd
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ org.reblochon.procrastinate
+ CFBundleURLSchemes
+
+ procrastinate
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ ITSAppUsesNonExemptEncryption
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/Program.cs b/10.0/Apps/Procrastinate/src/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..70a497193
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace procrastinate;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/10.0/Apps/Procrastinate/src/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 000000000..1ea3a5d73
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,51 @@
+
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Properties/launchSettings.json b/10.0/Apps/Procrastinate/src/Properties/launchSettings.json
new file mode 100644
index 000000000..f4c6c8ddd
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "Project",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/Resources/AppIcon/appicon.svg b/10.0/Apps/Procrastinate/src/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..f6e60da06
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/AppIcon/appiconfg.svg b/10.0/Apps/Procrastinate/src/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..46123af67
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Fonts/FontAwesome-Solid.otf b/10.0/Apps/Procrastinate/src/Resources/Fonts/FontAwesome-Solid.otf
new file mode 100644
index 000000000..6bd19b13b
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Fonts/FontAwesome-Solid.otf differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Regular.ttf b/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..26d4b1704
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Semibold.ttf b/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..d75d29dd9
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Images/dotnet_bot.png b/10.0/Apps/Procrastinate/src/Resources/Images/dotnet_bot.png
new file mode 100644
index 000000000..054167e59
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Images/dotnet_bot.png differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Raw/AboutAssets.txt b/10.0/Apps/Procrastinate/src/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..f22d3bfa8
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with your package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/10.0/Apps/Procrastinate/src/Resources/Raw/simon_blue.wav b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_blue.wav
new file mode 100644
index 000000000..634cc329f
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_blue.wav differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Raw/simon_green.wav b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_green.wav
new file mode 100644
index 000000000..fb5639f0d
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_green.wav differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Raw/simon_red.wav b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_red.wav
new file mode 100644
index 000000000..4aee5b69e
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_red.wav differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Raw/simon_yellow.wav b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_yellow.wav
new file mode 100644
index 000000000..3df6829f8
Binary files /dev/null and b/10.0/Apps/Procrastinate/src/Resources/Raw/simon_yellow.wav differ
diff --git a/10.0/Apps/Procrastinate/src/Resources/Splash/splash.svg b/10.0/Apps/Procrastinate/src/Resources/Splash/splash.svg
new file mode 100644
index 000000000..d4789bcf8
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Splash/splash.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.cs.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.cs.resx
new file mode 100644
index 000000000..4af5dda74
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.cs.resx
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Prokrastinovat
+ Nastavení
+ Přístupnost
+ Režim vysokého kontrastu
+ Použít barvy s vyšším kontrastem
+ Náhled motivu
+ Aktuální: {0} motiv
+ Výchozí
+ Vysoký kontrast
+ Změny se projeví okamžitě
+ Jazyk
+
+ Dnešní úkoly
+ Váš seznam produktivity:
+ Dát si pauzu
+ Jde vám to skvěle! Dnes jen 1 úkol!
+ Přidat další úkoly
+ 🎉 Gratulujeme! Dokončili jste VŠECHNY své úkoly!
+ Počkejte... stále potřebujete další pauzu!
+
+ Mini-hry
+ Protože produktivita je přeceňovaná!
+ Zamíchat hry
+ Skóre: {0}
+ Konec hry! Finální skóre: {0}
+ Začít
+ Nová hra
+ Váš tah (X)
+ AI přemýšlí...
+ Vyhráli jste! 🎉
+ AI vyhrála! 🤖
+ Remíza!
+ Kliknutí: {0}
+ Začít výzvu
+ Čekejte na zelenou!
+ KLEPNĚTE!
+ Příliš brzy! Zkuste znovu.
+ Reakční čas: {0}ms
+ Klepněte na 'Začít'
+
+ Generátor výmluv
+ Potřebujete důvod něco nedělat? Pomůžeme!
+ Klepněte pro novou výmluvu!
+ Generovat výmluvu
+ Kopírovat
+ Zkopírováno!
+ Dnes vygenerované výmluvy: {0}
+
+ Vaše statistiky
+ Buďte hrdí na své úspěchy!
+ Vyhnuté úkoly
+ Pauzy
+ Vygenerované výmluvy
+ Odehrané hry
+ Odemčený úspěch
+ Začátek: Otevřít aplikaci!
+ Začínající prokrastinátor
+ LEGENDÁRNÍ PROKRASTINÁTOR
+
+ Šimon říká
+ Sledujte vzor a opakujte!
+ Rychlost klikání
+ Kolik kliknutí za 5 sekund?
+ Reakční čas
+ Čekejte na zelenou a klepněte!
+ Piškvorky
+ Klasická hra X a O!
+ Hledání min
+ Najděte všechna bezpečná pole!
+ Naklápěcí had
+ Nakloňte telefon a veďte hada!
+ Pexeso
+ Najděte dvojice!
+ Hádej číslo
+ Hádejte číslo 1-100!
+ Mlátička krtků
+ Mlať krtky co nejrychleji!
+
+
+ Klikni na mě!
+ Konec: {0} kliknutí! ({1:F1}/sek)
+ Čekej...
+ Start
+ Příliš brzy!
+ Hotovo!
+ Najdi bezpečná pole! ({0} min)
+ Bum! Konec hry 💥
+ Nakloň pro pohyb!
+ Akcelerometr není k dispozici
+ Konec hry! Skóre: {0}
+ Tahy: {0} | Páry: {1}/{2}
+ Vyhráno za {0} tahů! 🎉
+ Myslím na číslo 1-100...
+ Pokusy: {0}
+ Zadej svůj tip
+ Hádat!
+ Zadej číslo 1-100
+ 🎉 Správně! Bylo to {0}!
+ 📈 {0} je příliš NÍZKÉ!
+ 📉 {0} je příliš VYSOKÉ!
+ Skóre: {0} | Minuto: {1}
+ Spustit hru (30s)
+
+
+ Motor výmluv
+ Vyberte, jak se generují výmluvy
+ Náhodný generátor
+ Cloud AI (Groq)
+ API Endpoint
+ http://localhost:11434
+ AI Model
+ Generuji...
+ Groq API klíč
+ Zadejte svůj Groq API klíč
+ Získejte svůj klíč zdarma na console.groq.com
+ AI na zařízení (Apple)
+ Používá Apple Intelligence na iOS 26+. Bez internetu, zcela soukromé.
+ Apple Intelligence je připravena!
+ Vyžaduje iOS 26+ s povolenou Apple Intelligence
+
+ Dát si šlofíka
+
+
+ Dát si kafe
+
+
+ Protáhnout se
+
+
+ Zírat na strop
+
+
+ O aplikaci
+
+
+ Ultimátní anti-produktivní aplikace. Postavena s .NET MAUI a zdravým pohrdáním termíny.
+
+
+ Vytvořil Stephane Delcroix
+
+ Sdílet
+ Sdílet tuto výmluvu
+ Posledních 7 Dní
+ Vygenerovat kreslený obrázek
+ Nepodařilo se vygenerovat obrázek. Zkuste to později.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.es.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.es.resx
new file mode 100644
index 000000000..9bf731f4f
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.es.resx
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Procrastinar
+ Ajustes
+ Accesibilidad
+ Modo alto contraste
+ Usar colores de mayor contraste
+ Vista previa del tema
+ Actual: Tema {0}
+ Predeterminado
+ Alto contraste
+ Los cambios se aplican inmediatamente
+ Idioma
+
+ Tareas de hoy
+ Tu lista de productividad:
+ Tomar un descanso
+ ¡Lo estás haciendo genial! ¡Solo 1 tarea hoy!
+ Añadir más tareas
+ 🎉 ¡Felicidades! ¡Completaste TODAS tus tareas!
+ Espera... ¡aún necesitas otro descanso!
+
+ Mini-Juegos
+ ¡Porque la productividad está sobrevalorada!
+ Mezclar juegos
+ Puntuación: {0}
+ ¡Fin del juego! Puntuación final: {0}
+ Empezar
+ Nuevo juego
+ Tu turno (X)
+ La IA está pensando...
+ ¡Ganaste! 🎉
+ ¡La IA gana! 🤖
+ ¡Empate!
+ Clics: {0}
+ Empezar desafío
+ ¡Espera el verde!
+ ¡TOCA AHORA!
+ ¡Muy pronto! Intenta de nuevo.
+ Tiempo de reacción: {0}ms
+ Toca 'Empezar' para comenzar
+
+ Generador de excusas
+ ¿Necesitas una razón para no hacer algo? ¡Te ayudamos!
+ ¡Toca el botón para una nueva excusa!
+ Generar excusa
+ Copiar
+ ¡Copiado!
+ Excusas generadas hoy: {0}
+
+ Tus estadísticas
+ ¡Siéntete orgulloso de tus logros!
+ Tareas evitadas
+ Descansos tomados
+ Excusas generadas
+ Juegos jugados
+ Logro desbloqueado
+ Inicio: ¡Abrir la app!
+ Procrastinador principiante
+ PROCRASTINADOR LEGENDARIO
+
+ Simón dice
+ ¡Observa el patrón y repítelo!
+ Velocidad de clic
+ ¿Cuántos clics en 5 segundos?
+ Tiempo de reacción
+ ¡Espera el verde y toca!
+ Tres en raya
+ ¡El clásico X y O!
+ Buscaminas
+ ¡Encuentra las celdas seguras!
+ Serpiente inclinable
+ ¡Inclina el teléfono para guiar la serpiente!
+ Memoria
+ ¡Encuentra los pares!
+ Adivina el número
+ ¡Adivina el número 1-100!
+ Golpea al topo
+ ¡Golpea los topos lo más rápido posible!
+
+
+ ¡Haz clic!
+ Final: {0} clics! ({1:F1}/seg)
+ Espera...
+ Iniciar
+ ¡Muy pronto!
+ ¡Listo!
+ ¡Encuentra las celdas seguras! ({0} minas)
+ ¡Boom! Fin del juego 💥
+ ¡Inclina para mover!
+ Acelerómetro no disponible
+ ¡Fin del juego! Puntuación: {0}
+ Movimientos: {0} | Pares: {1}/{2}
+ ¡Ganaste en {0} movimientos! 🎉
+ Estoy pensando en un número 1-100...
+ Intentos: {0}
+ Ingresa tu estimación
+ ¡Adivinar!
+ Ingresa un número 1-100
+ 🎉 ¡Correcto! ¡Era {0}!
+ 📈 ¡{0} es muy BAJO!
+ 📉 ¡{0} es muy ALTO!
+ Puntuación: {0} | Fallos: {1}
+ Iniciar Juego (30s)
+
+
+ Motor de excusas
+ Elige cómo se generan las excusas
+ Generador aleatorio
+ IA en la nube (Groq)
+ Punto de acceso API
+ http://localhost:11434
+ Modelo de IA
+ Generando...
+ Clave API Groq
+ Ingresa tu clave API Groq
+ Obtén tu clave gratis en console.groq.com
+ IA en dispositivo (Apple)
+ Usa Apple Intelligence en iOS 26+. Sin internet, completamente privado.
+ ¡Apple Intelligence está listo!
+ Requiere iOS 26+ con Apple Intelligence activado
+
+ Echarse una siesta
+
+
+ Tomar un café
+
+
+ Estirarse un poco
+
+
+ Mirar al techo
+
+
+ Acerca de
+
+
+ La aplicación anti-productividad definitiva. Construida con .NET MAUI y un sano desprecio por los plazos.
+
+
+ Hecho por Stephane Delcroix
+
+ Compartir
+ Compartir esta excusa
+ Últimos 7 Días
+ Generar dibujo
+ No se pudo generar la imagen. Inténtalo más tarde.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.fr.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.fr.resx
new file mode 100644
index 000000000..801477a20
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.fr.resx
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Procrastiner
+ Paramètres
+ Accessibilité
+ Mode contraste élevé
+ Utiliser des couleurs plus contrastées
+ Aperçu du thème
+ Actuel: Thème {0}
+ Par défaut
+ Contraste élevé
+ Les changements s'appliquent immédiatement
+ Langue
+
+ Tâches du jour
+ Votre liste de productivité:
+ Prendre une pause
+ Vous êtes super! Une seule tâche aujourd'hui!
+ Ajouter des tâches
+ 🎉 Félicitations! Vous avez terminé TOUTES vos tâches!
+ Attendez... vous avez encore besoin d'une pause!
+
+ Mini-Jeux
+ Parce que la productivité est surfaite!
+ Mélanger les jeux
+ Score: {0}
+ Fin de partie! Score final: {0}
+ Commencer
+ Nouvelle partie
+ Votre tour (X)
+ L'IA réfléchit...
+ Vous avez gagné! 🎉
+ L'IA gagne! 🤖
+ Match nul!
+ Clics: {0}
+ Commencer le défi
+ Attendez le vert!
+ APPUYEZ!
+ Trop tôt! Réessayez.
+ Temps de réaction: {0}ms
+ Appuyez sur 'Commencer'
+
+ Générateur d'excuses
+ Besoin d'une raison pour ne rien faire? On vous aide!
+ Appuyez pour une nouvelle excuse!
+ Générer une excuse
+ Copier
+ Copié!
+ Excuses générées aujourd'hui: {0}
+
+ Vos statistiques
+ Soyez fier de vos accomplissements!
+ Tâches évitées
+ Pauses prises
+ Excuses générées
+ Jeux joués
+ Succès débloqué
+ Début: Ouvrir l'appli!
+ Procrastinateur débutant
+ PROCRASTINATEUR LÉGENDAIRE
+
+ Jacques a dit
+ Observez et répétez le motif!
+ Vitesse de clic
+ Combien de clics en 5 secondes?
+ Temps de réaction
+ Attendez le vert, puis appuyez!
+ Morpion
+ Le classique X et O!
+ Démineur
+ Trouvez les cases sûres!
+ Serpent inclinable
+ Inclinez pour guider le serpent!
+ Memory
+ Trouvez les paires!
+ Devinez le nombre
+ Devinez le nombre 1-100!
+ Tape-taupe
+ Tapez les taupes rapidement!
+
+
+ Cliquez-moi!
+ Final: {0} clics! ({1:F1}/sec)
+ Attendez...
+ Démarrer
+ Trop tôt!
+ Terminé!
+ Trouvez les cellules sûres! ({0} mines)
+ Boom! Partie terminée 💥
+ Inclinez pour bouger!
+ Accéléromètre non disponible
+ Partie terminée! Score: {0}
+ Coups: {0} | Paires: {1}/{2}
+ Gagné en {0} coups! 🎉
+ Je pense à un nombre 1-100...
+ Tentatives: {0}
+ Entrez votre estimation
+ Deviner!
+ Entrez un nombre 1-100
+ 🎉 Correct! C'était {0}!
+ 📈 {0} est trop BAS!
+ 📉 {0} est trop HAUT!
+ Score: {0} | Ratés: {1}
+ Démarrer (30s)
+
+
+ Moteur d'excuses
+ Choisissez comment les excuses sont générées
+ Générateur aléatoire
+ IA Cloud (Groq)
+ Point d'accès API
+ http://localhost:11434
+ Modèle IA
+ Génération...
+ Clé API Groq
+ Entrez votre clé API Groq
+ Obtenez votre clé gratuite sur console.groq.com
+ IA sur l'appareil (Apple)
+ Utilise Apple Intelligence sur iOS 26+. Aucune connexion internet requise, totalement privé.
+ Apple Intelligence est prêt!
+ Nécessite iOS 26+ avec Apple Intelligence activé
+
+ Faire une sieste
+
+
+ Prendre un café
+
+
+ S'étirer un peu
+
+
+ Fixer le plafond
+
+
+ À propos
+
+
+ L'application anti-productivité ultime. Construite avec .NET MAUI et un sain mépris des délais.
+
+
+ Créé par Stephane Delcroix
+
+ Partager
+ Partager cette excuse
+ 7 Derniers Jours
+ Générer un dessin
+ Impossible de générer l'image. Réessayez plus tard.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.nl.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.nl.resx
new file mode 100644
index 000000000..ae7ec34a3
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.nl.resx
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Uitstellen
+ Instellingen
+ Toegankelijkheid
+ Hoog contrast modus
+ Gebruik kleuren met hoger contrast
+ Thema voorbeeld
+ Huidig: {0} Thema
+ Standaard
+ Hoog contrast
+ Wijzigingen worden direct toegepast
+ Taal
+
+ Taken van vandaag
+ Je productiviteitslijst:
+ Neem een pauze
+ Je doet het geweldig! Maar 1 taak vandaag!
+ Meer taken toevoegen
+ 🎉 Gefeliciteerd! Je hebt AL je taken voltooid!
+ Wacht... je hebt nog een pauze nodig!
+
+ Mini-Spelletjes
+ Omdat productiviteit overschat wordt!
+ Spelletjes shuffelen
+ Score: {0}
+ Game Over! Eindscore: {0}
+ Starten
+ Nieuw spel
+ Jouw beurt (X)
+ AI denkt na...
+ Je hebt gewonnen! 🎉
+ AI wint! 🤖
+ Gelijkspel!
+ Klikken: {0}
+ Start uitdaging
+ Wacht op groen!
+ TIK NU!
+ Te vroeg! Probeer opnieuw.
+ Reactietijd: {0}ms
+ Tik op 'Starten' om te beginnen
+
+ Excuusgenerator
+ Een reden nodig om iets niet te doen? Wij helpen!
+ Tik voor een nieuw excuus!
+ Genereer excuus
+ Kopiëren
+ Gekopieerd!
+ Excuses gegenereerd vandaag: {0}
+
+ Jouw statistieken
+ Wees trots op je prestaties!
+ Taken vermeden
+ Pauzes genomen
+ Excuses gegenereerd
+ Spelletjes gespeeld
+ Prestatie ontgrendeld
+ Begin: Open de app!
+ Beginner uitstelaar
+ LEGENDARISCHE UITSTELAAR
+
+ Simon zegt
+ Kijk naar het patroon en herhaal!
+ Kliksnelheid
+ Hoeveel klikken in 5 seconden?
+ Reactietijd
+ Wacht op groen en tik!
+ Boter-kaas-en-eieren
+ Het klassieke X en O spel!
+ Mijnenveger
+ Vind alle veilige cellen!
+ Kantel slang
+ Kantel je telefoon om de slang te leiden!
+ Memory
+ Vind de paren!
+ Raad het getal
+ Raad het getal 1-100!
+ Mep de mol
+ Mep de mollen zo snel mogelijk!
+
+
+ Klik op mij!
+ Eindstand: {0} klikken! ({1:F1}/sec)
+ Wacht...
+ Start
+ Te vroeg!
+ Klaar!
+ Vind de veilige cellen! ({0} mijnen)
+ Boem! Spel voorbij 💥
+ Kantel om te bewegen!
+ Accelerometer niet beschikbaar
+ Spel voorbij! Score: {0}
+ Zetten: {0} | Paren: {1}/{2}
+ Gewonnen in {0} zetten! 🎉
+ Ik denk aan een getal 1-100...
+ Pogingen: {0}
+ Voer je gok in
+ Raden!
+ Voer een getal 1-100 in
+ 🎉 Correct! Het was {0}!
+ 📈 {0} is te LAAG!
+ 📉 {0} is te HOOG!
+ Score: {0} | Gemist: {1}
+ Start Spel (30s)
+
+
+ Excuusgenerator
+ Kies hoe excuses worden gegenereerd
+ Willekeurige generator
+ Cloud AI (Groq)
+ API Endpoint
+ http://localhost:11434
+ AI Model
+ Genereren...
+ Groq API-sleutel
+ Voer uw Groq API-sleutel in
+ Haal uw gratis sleutel op console.groq.com
+ On-Device AI (Apple)
+ Gebruikt Apple Intelligence op iOS 26+. Geen internet nodig, volledig privé.
+ Apple Intelligence is klaar!
+ Vereist iOS 26+ met Apple Intelligence ingeschakeld
+
+ Doe een dutje
+
+
+ Pak een koffie
+
+
+ Strek je even
+
+
+ Staar naar het plafond
+
+
+ Over
+
+
+ De ultieme anti-productiviteitsapp. Gebouwd met .NET MAUI en een gezonde minachting voor deadlines.
+
+
+ Gemaakt door Stephane Delcroix
+
+ Delen
+ Deel dit excuus
+ Laatste 7 Dagen
+ Genereer cartoon
+ Kon afbeelding niet genereren. Probeer later opnieuw.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.pt.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.pt.resx
new file mode 100644
index 000000000..525cd06a3
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.pt.resx
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Procrastinar
+ Configurações
+ Acessibilidade
+ Modo alto contraste
+ Usar cores de maior contraste
+ Prévia do tema
+ Atual: Tema {0}
+ Padrão
+ Alto contraste
+ As alterações se aplicam imediatamente
+ Idioma
+
+ Tarefas de hoje
+ Sua lista de produtividade:
+ Fazer uma pausa
+ Você está indo muito bem! Só 1 tarefa hoje!
+ Adicionar mais tarefas
+ 🎉 Parabéns! Você completou TODAS as suas tarefas!
+ Espere... você ainda precisa de outra pausa!
+
+ Mini-Jogos
+ Porque produtividade é superestimada!
+ Embaralhar jogos
+ Pontuação: {0}
+ Fim de jogo! Pontuação final: {0}
+ Começar
+ Novo jogo
+ Sua vez (X)
+ A IA está pensando...
+ Você ganhou! 🎉
+ A IA ganhou! 🤖
+ Empate!
+ Cliques: {0}
+ Iniciar desafio
+ Espere pelo verde!
+ TOQUE AGORA!
+ Muito cedo! Tente novamente.
+ Tempo de reação: {0}ms
+ Toque em 'Começar' para iniciar
+
+ Gerador de desculpas
+ Precisa de uma razão para não fazer algo? Nós ajudamos!
+ Toque no botão para uma nova desculpa!
+ Gerar desculpa
+ Copiar
+ Copiado!
+ Desculpas geradas hoje: {0}
+
+ Suas estatísticas
+ Tenha orgulho das suas conquistas!
+ Tarefas evitadas
+ Pausas feitas
+ Desculpas geradas
+ Jogos jogados
+ Conquista desbloqueada
+ Início: Abrir o app!
+ Procrastinador iniciante
+ PROCRASTINADOR LENDÁRIO
+
+ Simon diz
+ Observe o padrão e repita!
+ Velocidade de clique
+ Quantos cliques em 5 segundos?
+ Tempo de reação
+ Espere pelo verde e toque!
+ Jogo da velha
+ O clássico X e O!
+ Campo minado
+ Encontre as células seguras!
+ Cobra inclinável
+ Incline o telefone para guiar a cobra!
+ Jogo da memória
+ Encontre os pares!
+ Adivinhe o número
+ Adivinhe o número 1-100!
+ Acerte a toupeira
+ Acerte as toupeiras o mais rápido possível!
+
+
+ Clique em mim!
+ Final: {0} cliques! ({1:F1}/seg)
+ Aguarde...
+ Iniciar
+ Muito cedo!
+ Pronto!
+ Encontre as células seguras! ({0} minas)
+ Boom! Fim de jogo 💥
+ Incline para mover!
+ Acelerômetro não disponível
+ Fim de jogo! Pontuação: {0}
+ Movimentos: {0} | Pares: {1}/{2}
+ Você ganhou em {0} movimentos! 🎉
+ Estou pensando em um número 1-100...
+ Tentativas: {0}
+ Digite seu palpite
+ Adivinhar!
+ Digite um número 1-100
+ 🎉 Correto! Era {0}!
+ 📈 {0} é muito BAIXO!
+ 📉 {0} é muito ALTO!
+ Pontuação: {0} | Erros: {1}
+ Iniciar Jogo (30s)
+
+
+ Motor de desculpas
+ Escolha como as desculpas são geradas
+ Gerador aleatório
+ IA na nuvem (Groq)
+ Endpoint da API
+ http://localhost:11434
+ Modelo de IA
+ Gerando...
+ Chave API Groq
+ Digite sua chave API Groq
+ Obtenha sua chave grátis em console.groq.com
+ IA no dispositivo (Apple)
+ Usa Apple Intelligence no iOS 26+. Sem internet, totalmente privado.
+ Apple Intelligence está pronto!
+ Requer iOS 26+ com Apple Intelligence ativado
+
+ Tirar uma soneca
+
+
+ Tomar um café
+
+
+ Alongar um pouco
+
+
+ Olhar para o teto
+
+
+ Sobre
+
+
+ O aplicativo anti-produtividade definitivo. Construído com .NET MAUI e um saudável desprezo por prazos.
+
+
+ Feito por Stephane Delcroix
+
+ Compartilhar
+ Compartilhar esta desculpa
+ Últimos 7 Dias
+ Gerar desenho
+ Não foi possível gerar a imagem. Tente novamente mais tarde.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.resx
new file mode 100644
index 000000000..4233890bd
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.resx
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Procrastinate
+ Settings
+ Tasks
+ Games
+ Excuses
+ Stats
+ Appearance
+ Light Theme
+ Switch to Nord Light color scheme
+ Dark
+ Light
+ Zalgo Randomly
+ Chaos appears when you least expect it
+ Theme Preview
+ Current: Nord {0}
+ Changes apply immediately
+ Language
+
+
+ Today's Tasks
+ Your productivity list:
+ Take a break
+ You're doing great! Only 1 task today!
+ Add More Tasks
+ 🎉 Congratulations! You completed ALL your tasks!
+ Wait... you still need another break!
+
+
+ Mini-Games
+ Because productivity is overrated!
+ Shuffle Games
+ Score: {0}
+ Game Over! Final Score: {0}
+ Start Game
+ New Game
+ Your turn (X)
+ AI thinking...
+ You win! 🎉
+ AI wins! 🤖
+ It's a draw!
+ Clicks: {0}
+ Start Challenge
+ Wait for green!
+ TAP NOW!
+ Too early! Try again.
+ Reaction time: {0}ms
+ Minifig Creator
+ Generate random minifigures!
+ Generate
+ A brave adventurer from Brickland!
+ Expert builder with 10 years experience.
+ Dreams of becoming a master builder.
+ Collector of rare bricks since childhood.
+ Known for incredible MOC creations.
+ Eggs Catch
+ Catch falling eggs with your basket!
+ Tap 'Start' to begin
+
+
+ Excuse Generator
+ Need a reason to not do something? We got you!
+ Tap the button for a fresh excuse!
+ Generate Excuse
+ Copy to Clipboard
+ Copied!
+ Excuses generated today: {0}
+
+
+ Your Stats
+ Be proud of your accomplishments!
+ Tasks Avoided
+ Breaks Taken
+ Excuses Generated
+ Games Played
+ Achievement Unlocked
+ Getting Started: Open the app!
+ Beginner Procrastinator
+ LEGENDARY PROCRASTINATOR
+
+
+ Simon Says
+ Watch the pattern and repeat it!
+ Click Speed
+ How many clicks in 5 seconds?
+ Reaction Time
+ Wait for green, then tap!
+ Tic Tac Toe
+ Classic X and O game!
+ Minesweeper
+ Find all safe cells!
+ Tilt Snake
+ Tilt your phone to guide the snake!
+ Memory Match
+ Find matching pairs!
+ Number Guess
+ Guess the number 1-100!
+ Whack-a-Mole
+ Tap the moles as fast as you can!
+
+
+ Click Me!
+ Final: {0} clicks! ({1:F1}/sec)
+ Wait...
+ Start
+ Too soon!
+ Done!
+ Find the safe cells! ({0} mines)
+ Boom! Game over 💥
+ Tilt phone to move!
+ Accelerometer not available
+ Game Over! Score: {0}
+ Moves: {0} | Pairs: {1}/{2}
+ You win in {0} moves! 🎉
+ I'm thinking of a number 1-100...
+ Attempts: {0}
+ Enter your guess
+ Guess!
+ Please enter a number 1-100
+ 🎉 Correct! It was {0}!
+ 📈 {0} is too LOW!
+ 📉 {0} is too HIGH!
+ Score: {0} | Misses: {1}
+ Start Game (30s)
+
+
+ Excuse Engine
+ Choose how excuses are generated
+ Random Generator
+ Cloud AI (Groq)
+ API Endpoint
+ http://localhost:11434
+ AI Model
+ Generating...
+ Groq API Key
+ Enter your Groq API key
+ Get your free API key at console.groq.com
+ On-Device AI (Apple)
+ Uses Apple Intelligence on iOS 26+. No internet required, completely private.
+ Apple Intelligence is ready!
+ Requires iOS 26+ with Apple Intelligence enabled
+
+
+ AI API Calls
+ High Scores
+ No high scores yet. Play some games!
+
+ Take a nap
+
+
+ Grab a coffee
+
+
+ Stretch a bit
+
+
+ Stare at the ceiling
+
+
+ About
+
+
+ The ultimate anti-productivity app. Built with .NET MAUI and a healthy disregard for deadlines.
+
+
+ Made by Stephane Delcroix
+
+ Share
+ Share this excuse
+ Last 7 Days
+ Total Clicks
+ Generate Cartoon
+ Could not generate image. Try again later.
+
diff --git a/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.uk.resx b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.uk.resx
new file mode 100644
index 000000000..5cd44e8c4
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Strings/AppResources.uk.resx
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Прокрастинація
+ Налаштування
+ Завдання
+ Ігри
+ Виправдання
+ Статистика
+ Зовнішній вигляд
+ Світла тема
+ Перемкнутися на світлу колірну схему Nord
+ Темна
+ Світла
+ Випадковий Zalgo
+ Хаос з'являється, коли ви найменше на це очікуєте
+ Попередній перегляд теми
+ Поточна: Nord {0}
+ Зміни застосовуються миттєво
+ Мова
+
+ Завдання на сьогодні
+ Ваш список продуктивності:
+ Зробіть перерву
+ Ви чудово справляєтесь! Лише 1 завдання на сьогодні!
+ Додати більше завдань
+ 🎉 Вітаємо! Ви виконали ВСІ свої завдання!
+ Зачекайте... вам потрібна ще одна перерва!
+
+ Міні-ігри
+ Бо продуктивність — це переоцінена річ!
+ Перемішати ігри
+ Рахунок: {0}
+ Гра закінчена! Фінальний рахунок: {0}
+ Почати гру
+ Нова гра
+ Ваш хід (X)
+ ШІ думає...
+ Ви перемогли! 🎉
+ ШІ переміг! 🤖
+ Нічия!
+ Кліки: {0}
+ Почати виклик
+ Чекайте на зелений!
+ ТИСНІТЬ ЗАРАЗ!
+ Занадто рано! Спробуйте ще раз.
+ Час реакції: {0} мс
+ Творець мініфігурок
+ Створюйте випадкові мініфігурки!
+ Згенерувати
+ Хоробрий шукач пригод із Брікленда!
+ Експерт-будівельник з 10-річним досвідом.
+ Мріє стати майстром-будівельником.
+ Колекціонер рідкісних цеглинок з дитинства.
+ Відомий своїми неймовірними MOC-творіннями.
+ Лови яйця
+ Ловіть яйця, що падають, у свій кошик!
+ Натисніть «Старт», щоб почати
+
+ Генератор виправдань
+ Потрібна причина, щоб нічого не робити? Ми допоможемо!
+ Натисніть кнопку, щоб отримати нове виправдання!
+ Згенерувати виправдання
+ Копіювати в буфер
+ Скопійовано!
+ Виправдань згенеровано сьогодні: {0}
+
+ Ваша статистика
+ Пишайтеся своїми досягненнями!
+ Проігноровані завдання
+ Зроблені перерви
+ Згенеровані виправдання
+ Зіграні ігри
+ Досягнення розблоковано
+ Початок покладено: Відкрито додаток!
+ Прокрастинатор-початківець
+ ЛЕГЕНДАРНИЙ ПРОКРАСТИНАТОР
+
+ Саймон каже
+ Спостерігайте за послідовністю та повторюйте її!
+ Швидкість кліків
+ Скільки кліків за 5 секунд?
+ Час реакції
+ Чекайте на зелений, потім тисніть!
+ Хрестики-нулики
+ Класична гра в хрестики та нулики!
+ Сапер
+ Знайдіть усі безпечні клітинки!
+ Змійка нахилом
+ Нахиляйте телефон, щоб керувати змійкою!
+ Пари пам'яті
+ Знайдіть однакові пари!
+ Вгадай число
+ Вгадайте число від 1 до 100!
+ Впіймай крота
+ Тицяйте на кротів якомога швидше!
+
+ Тисни мене!
+ Разом: {0} кліків! ({1:F1}/сек)
+ Чекайте...
+ Старт
+ Занадто рано!
+ Готово!
+ Знайдіть безпечні клітинки! ({0} мін)
+ Бум! Гру закінчено 💥
+ Нахиляйте телефон для руху!
+ Акселерометр недоступний
+ Гра закінчена! Рахунок: {0}
+ Ходи: {0} | Пари: {1}/{2}
+ Ви перемогли за {0} ходів! 🎉
+ Я загадав число від 1 до 100...
+ Спроби: {0}
+ Введіть число
+ Вгадати!
+ Будь ласка, введіть число від 1 до 100
+ 🎉 Правильно! Це було {0}!
+ 📈 {0} — це замало!
+ 📉 {0} — це забагато!
+ Рахунок: {0} | Промахи: {1}
+ Почати гру (30с)
+
+ Рушій виправдань
+ Оберіть, як генеруються виправдання
+ Випадковий генератор
+ Хмарний ШІ (Groq)
+ Кінцева точка API
+ http://localhost:11434
+ Модель ШІ
+ Генерація...
+ Ключ API Groq
+ Введіть ваш API ключ Groq
+ Отримайте безкоштовний ключ на console.groq.com
+ Локальний ШІ (Apple)
+ Використовує Apple Intelligence на iOS 26+. Інтернет не потрібен, повна приватність.
+ Apple Intelligence готовий до роботи!
+ Потрібна iOS 26+ з увімкненим Apple Intelligence
+
+ Запити до AI API
+ Рекорди
+ Рекордів ще немає. Зіграйте в якусь гру!
+ Поспіть трохи
+ Випийте кави
+ Трохи розімніться
+ Подивіться у стелю
+ Про програму
+ Найкращий антипродуктивний додаток. Побудований на .NET MAUI з повним ігноруванням дедлайнів.
+ Автор: Стефан Делькруа (Stephane Delcroix)
+ Поділитися
+ Поділитися цим виправданням
+ Останні 7 днів
+ Згенерувати комікс
+ Не вдалося згенерувати зображення. Спробуйте пізніше.
+
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/Resources/Styles/Colors.xaml b/10.0/Apps/Procrastinate/src/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..11f4cb930
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Styles/Colors.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+ #2E3440
+ #3B4252
+ #434C5E
+ #4C566A
+
+
+ #D8DEE9
+ #E5E9F0
+ #ECEFF4
+
+
+ #8FBCBB
+ #88C0D0
+ #81A1C1
+ #5E81AC
+
+
+ #BF616A
+ #D08770
+ #EBCB8B
+ #A3BE8C
+ #B48EAD
+
+
+ #FFFFFF
+ #000000
+ Transparent
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/10.0/Apps/Procrastinate/src/Resources/Styles/Styles.xaml b/10.0/Apps/Procrastinate/src/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..927520553
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Resources/Styles/Styles.xaml
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/10.0/Apps/Procrastinate/src/Services/AgentPipelineExcuseGenerator.cs b/10.0/Apps/Procrastinate/src/Services/AgentPipelineExcuseGenerator.cs
new file mode 100644
index 000000000..93990a3fe
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Services/AgentPipelineExcuseGenerator.cs
@@ -0,0 +1,159 @@
+using System.Diagnostics;
+using Microsoft.Extensions.AI;
+
+namespace procrastinate.Services;
+
+///
+/// Multi-agent excuse pipeline: Researcher → Writer → Editor.
+/// Each agent is a separate IChatClient call, chained sequentially.
+/// Reports progress via the OnStageChanged callback.
+///
+public class AgentPipelineExcuseGenerator : IExcuseGenerator
+{
+ private readonly IChatClient? _onDeviceChatClient;
+
+ public string Name => "AI Agent Pipeline";
+ public bool IsAvailable => _onDeviceChatClient is not null || CloudExcuseGenerator.IsCloudAvailable;
+
+ ///
+ /// Callback fired when the pipeline moves to a new stage.
+ ///
+ public Action? OnStageChanged { get; set; }
+
+ ///
+ /// Callback fired when an agent produces output. (stageName, agentOutput)
+ ///
+ public Action? OnAgentOutput { get; set; }
+
+ public AgentPipelineExcuseGenerator(IChatClient? onDeviceChatClient = null)
+ {
+ _onDeviceChatClient = onDeviceChatClient;
+ }
+
+ public async Task GenerateExcuseAsync(string language)
+ {
+ var stopwatch = Stopwatch.StartNew();
+
+ IChatClient? client = _onDeviceChatClient;
+ IChatClient? disposableClient = null;
+ var modelName = "Apple Intelligence";
+
+ if (client is null && CloudExcuseGenerator.IsCloudAvailable)
+ {
+ disposableClient = CloudExcuseGenerator.CreateChatClient();
+ client = disposableClient;
+ modelName = "Cloud AI";
+ }
+
+ if (client is null)
+ {
+ stopwatch.Stop();
+ return new ExcuseResult("No AI available. Configure Cloud AI or use a device with Apple Intelligence.", Name, stopwatch.Elapsed);
+ }
+
+ try
+ {
+ var languageName = GetLanguageName(language);
+
+ // Agent 1: Researcher — picks an absurd scenario
+ OnStageChanged?.Invoke("🔍 Agent 1: Researching...");
+ var scenario = await RunAgentAsync(client,
+ "You are a creative comedy researcher. Your job is to come up with an absurd, unexpected scenario that could be used as an excuse. Output ONLY the scenario in 1-2 sentences. Be wildly creative.",
+ $"Come up with a bizarre, funny scenario involving {GetRandomElement()}. Make it unexpected and absurd. Just the scenario, nothing else.");
+ OnAgentOutput?.Invoke("🔍 Researcher", scenario);
+
+ // Agent 2: Writer — crafts the excuse from the scenario
+ OnStageChanged?.Invoke("✍️ Agent 2: Writing...");
+ var rawExcuse = await RunAgentAsync(client,
+ "You are a comedy writer. You turn scenarios into first-person excuses that sound like something a real person would say. Keep it to 1-2 sentences. Start with 'I' or 'Sorry'.",
+ $"Turn this scenario into a funny first-person excuse in {languageName}:\n\n{scenario}\n\nJust the excuse, nothing else.");
+ OnAgentOutput?.Invoke("✍️ Writer", rawExcuse);
+
+ // Agent 3: Editor — polishes and ensures quality
+ OnStageChanged?.Invoke("✨ Agent 3: Polishing...");
+ var finalExcuse = await RunAgentAsync(client,
+ $"You are a comedy editor. You polish excuses to be funnier and more natural-sounding. Keep the same language ({languageName}). Output ONLY the polished excuse.",
+ $"Polish this excuse to be funnier and more natural. Keep it in {languageName}. Keep it to 1-2 sentences:\n\n{rawExcuse}\n\nJust the polished excuse, nothing else.");
+ OnAgentOutput?.Invoke("✨ Editor", finalExcuse.Trim());
+
+ OnStageChanged?.Invoke("✅ Done!");
+ stopwatch.Stop();
+
+ return new ExcuseResult(
+ finalExcuse.Trim(),
+ Name,
+ stopwatch.Elapsed,
+ Model: $"{modelName} (3 agents)");
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ System.Diagnostics.Debug.WriteLine($"Pipeline error: {ex}");
+
+ var friendly = ex.Message switch
+ {
+ string m when m.Contains("content", StringComparison.OrdinalIgnoreCase) &&
+ m.Contains("unsafe", StringComparison.OrdinalIgnoreCase)
+ => "The AI thought that excuse was too spicy! Try again for a tamer one. 🌶️",
+ string m when m.Contains("content", StringComparison.OrdinalIgnoreCase) &&
+ m.Contains("filter", StringComparison.OrdinalIgnoreCase)
+ => "The AI's content filter kicked in — apparently that excuse was TOO creative. Try again! 🎨",
+ string m when m.Contains("timeout", StringComparison.OrdinalIgnoreCase) ||
+ m.Contains("timed out", StringComparison.OrdinalIgnoreCase)
+ => "Even the AI is procrastinating! It took too long. Try again. ⏰",
+ string m when m.Contains("network", StringComparison.OrdinalIgnoreCase) ||
+ m.Contains("connection", StringComparison.OrdinalIgnoreCase)
+ => "No connection — the AI agents are on a coffee break. Check your network. ☕",
+ string m when m.Contains("rate limit", StringComparison.OrdinalIgnoreCase) ||
+ m.Contains("429", StringComparison.OrdinalIgnoreCase)
+ => "Too many excuses requested! The AI needs a breather. Wait a moment. 😮💨",
+ _ => "The excuse factory had a hiccup. Give it another shot! 🏭"
+ };
+
+ return new ExcuseResult(friendly, Name, stopwatch.Elapsed);
+ }
+ finally
+ {
+ (disposableClient as IDisposable)?.Dispose();
+ }
+ }
+
+ private static async Task RunAgentAsync(IChatClient client, string systemPrompt, string userPrompt)
+ {
+ var messages = new List
+ {
+ new(ChatRole.System, systemPrompt),
+ new(ChatRole.User, userPrompt)
+ };
+
+ var response = await client.GetResponseAsync(messages);
+ return response.Text?.Trim() ?? "";
+ }
+
+ private static string GetRandomElement()
+ {
+ var elements = new[]
+ {
+ "a time-traveling pigeon", "sentient office furniture", "a secret society of squirrels",
+ "a parallel universe where gravity is optional", "a conspiracy involving socks",
+ "an AI that became a life coach", "a haunted coffee machine", "quantum entangled twins",
+ "a diplomatic incident with a penguin", "a rogue weather satellite",
+ "an enchanted parking meter", "a philosophical debate with a cat",
+ "a mysterious portal in the closet", "an accidental invention", "a cursed alarm clock",
+ "a runaway sourdough starter", "an overly helpful robot vacuum",
+ "a neighborhood raccoon uprising", "a telepathic houseplant", "a glitch in spacetime"
+ };
+ return elements[Random.Shared.Next(elements.Length)];
+ }
+
+ private static string GetLanguageName(string language) => language switch
+ {
+ "fr" => "French",
+ "es" => "Spanish",
+ "pt" => "Portuguese",
+ "nl" => "Dutch",
+ "cs" => "Czech",
+ "uk" => "Ukrainian",
+ _ => "English"
+ };
+}
diff --git a/10.0/Apps/Procrastinate/src/Services/AppStrings.cs b/10.0/Apps/Procrastinate/src/Services/AppStrings.cs
new file mode 100644
index 000000000..0507b248c
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Services/AppStrings.cs
@@ -0,0 +1,263 @@
+using System.ComponentModel;
+using System.Globalization;
+using System.Resources;
+
+namespace procrastinate.Services;
+
+public class AppStrings : INotifyPropertyChanged
+{
+ private static readonly Lazy _instance = new(() => new AppStrings());
+ public static AppStrings Instance => _instance.Value;
+
+ private static readonly ResourceManager _resourceManager =
+ new("procrastinate.Resources.Strings.AppResources", typeof(AppStrings).Assembly);
+
+ private CultureInfo _culture;
+ private bool _zalgoMode;
+
+ // Zalgo combining characters
+ private static readonly char[] ZalgoUp = {
+ '\u0300', '\u0301', '\u0302', '\u0303', '\u0304', '\u0305', '\u0306', '\u0307',
+ '\u0308', '\u0309', '\u030a', '\u030b', '\u030c', '\u030d', '\u030e', '\u030f',
+ '\u0310', '\u0311', '\u0312', '\u0313', '\u0314', '\u0315', '\u031a', '\u031b',
+ '\u033d', '\u033e', '\u033f', '\u0340', '\u0341', '\u0342', '\u0343', '\u0344'
+ };
+ private static readonly char[] ZalgoDown = {
+ '\u0316', '\u0317', '\u0318', '\u0319', '\u031c', '\u031d', '\u031e', '\u031f',
+ '\u0320', '\u0321', '\u0322', '\u0323', '\u0324', '\u0325', '\u0326', '\u0327',
+ '\u0328', '\u0329', '\u032a', '\u032b', '\u032c', '\u032d', '\u032e', '\u032f',
+ '\u0330', '\u0331', '\u0332', '\u0333', '\u0339', '\u033a', '\u033b', '\u033c'
+ };
+ private static readonly char[] ZalgoMid = { '\u0334', '\u0335', '\u0336', '\u0337', '\u0338' };
+ private static readonly Random _random = new();
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public static readonly Dictionary SupportedLanguages = new()
+ {
+ { "", "System Default" },
+ { "en", "English" },
+ { "fr", "Français" },
+ { "es", "Español" },
+ { "pt", "Português" },
+ { "nl", "Nederlands" },
+ { "cs", "Čeština" },
+ { "uk", "Українська" }
+ };
+
+ private AppStrings()
+ {
+ var savedLang = Preferences.Get("AppLanguage", "");
+ if (string.IsNullOrEmpty(savedLang))
+ {
+ // Use system language, fall back to English if not supported
+ var systemLang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
+ savedLang = SupportedLanguages.ContainsKey(systemLang) ? systemLang : "en";
+ }
+ _culture = new CultureInfo(savedLang);
+ _zalgoMode = Preferences.Get("ZalgoMode", true); // Default ON
+ }
+
+ public static string CurrentLanguage
+ {
+ get => Preferences.Get("AppLanguage", "");
+ set
+ {
+ Preferences.Set("AppLanguage", value);
+ if (string.IsNullOrEmpty(value))
+ {
+ // Use system language, fall back to English if not supported
+ var systemLang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
+ var effectiveLang = SupportedLanguages.ContainsKey(systemLang) ? systemLang : "en";
+ Instance._culture = new CultureInfo(effectiveLang);
+ }
+ else
+ {
+ Instance._culture = new CultureInfo(value);
+ }
+ Instance.OnPropertyChanged(null);
+ }
+ }
+
+ ///
+ /// Returns the actual language being used (resolves "System Default" to the actual language code)
+ ///
+ public static string EffectiveLanguage
+ {
+ get
+ {
+ var saved = Preferences.Get("AppLanguage", "");
+ if (!string.IsNullOrEmpty(saved))
+ return saved;
+
+ // Resolve system language
+ var systemLang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
+ return SupportedLanguages.ContainsKey(systemLang) ? systemLang : "en";
+ }
+ }
+
+ public static bool IsZalgoMode
+ {
+ get => Instance._zalgoMode;
+ set
+ {
+ Instance._zalgoMode = value;
+ Preferences.Set("ZalgoMode", value);
+ Instance.OnPropertyChanged(null);
+ }
+ }
+
+ public string this[string key] => GetString(key);
+
+ public static string Zalgoify(string text)
+ {
+ if (string.IsNullOrEmpty(text)) return text;
+
+ var result = new System.Text.StringBuilder();
+ bool inPlaceholder = false;
+
+ foreach (char c in text)
+ {
+ // Track if we're inside a format placeholder like {0}
+ if (c == '{') inPlaceholder = true;
+ else if (c == '}') inPlaceholder = false;
+
+ result.Append(c);
+
+ // Only add zalgo to letters/digits outside of placeholders
+ if (!inPlaceholder && c != '}' && char.IsLetterOrDigit(c))
+ {
+ // Add 0-2 combining characters above, middle, and below
+ for (int i = 0; i < _random.Next(0, 3); i++)
+ result.Append(ZalgoUp[_random.Next(ZalgoUp.Length)]);
+ for (int i = 0; i < _random.Next(0, 2); i++)
+ result.Append(ZalgoMid[_random.Next(ZalgoMid.Length)]);
+ for (int i = 0; i < _random.Next(0, 3); i++)
+ result.Append(ZalgoDown[_random.Next(ZalgoDown.Length)]);
+ }
+ }
+ return result.ToString();
+ }
+
+ // Break message variations - randomly picks one
+ private static readonly string[] BreakMessageKeys = { "TakeABreak", "TakeANap", "GrabACoffee", "StretchABit", "StareAtCeiling" };
+
+ private string GetRandomBreakMessage()
+ {
+ var key = BreakMessageKeys[_random.Next(BreakMessageKeys.Length)];
+ return this[key];
+ }
+
+ public static string GetString(string key)
+ {
+ var text = _resourceManager.GetString(key, Instance._culture) ?? key;
+ // When zalgo mode is on, apply randomly 8% of the time for readability
+ // When off, never apply zalgo
+ var applyZalgo = Instance._zalgoMode && _random.Next(100) < 8;
+ return applyZalgo ? Zalgoify(text) : text;
+ }
+
+ public static string GetString(string key, params object[] args)
+ {
+ var format = GetString(key);
+ return string.Format(format, args);
+ }
+
+ // For backwards compatibility
+ public static string Get(string key) => GetString(key);
+ public static string Get(string key, params object[] args) => GetString(key, args);
+
+ ///
+ /// Refresh all string bindings to recompute zalgo randomness
+ /// Call this on navigation, button clicks, etc.
+ ///
+ public static void Refresh()
+ {
+ Instance.OnPropertyChanged(null);
+ }
+
+ protected void OnPropertyChanged(string? propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ // Properties for direct XAML binding
+ public string AppName => this["AppName"];
+ public string Settings => this["Settings"];
+ public string TabTasks => this["TabTasks"];
+ public string TabGames => this["TabGames"];
+ public string TabExcuses => this["TabExcuses"];
+ public string TabStats => this["TabStats"];
+ public string Accessibility => this["Accessibility"];
+ public string HighContrastMode => this["HighContrastMode"];
+ public string HighContrastDesc => this["HighContrastDesc"];
+ public string DefaultTheme => this["DefaultTheme"];
+ public string HighContrast => this["HighContrast"];
+ public string ZalgoMode => this["ZalgoMode"];
+ public string ZalgoModeDesc => this["ZalgoModeDesc"];
+ public string ThemePreview => this["ThemePreview"];
+ public string ChangesApply => this["ChangesApply"];
+ public string Language => this["Language"];
+ public string TodaysTasks => this["TodaysTasks"];
+ public string YourProductivityList => this["YourProductivityList"];
+ public string TakeABreak => GetRandomBreakMessage();
+ public string DoingGreat => this["DoingGreat"];
+ public string AddMoreTasks => this["AddMoreTasks"];
+ public string Congratulations => this["Congratulations"];
+ public string NeedAnotherBreak => this["NeedAnotherBreak"];
+ public string MiniGames => this["MiniGames"];
+ public string ProductivityOverrated => this["ProductivityOverrated"];
+ public string ShuffleGames => this["ShuffleGames"];
+ public string ExcuseGenerator => this["ExcuseGenerator"];
+ public string NeedAReason => this["NeedAReason"];
+ public string TapForExcuse => this["TapForExcuse"];
+ public string GenerateExcuse => this["GenerateExcuse"];
+ public string CopyToClipboard => this["CopyToClipboard"];
+ public string YourStats => this["YourStats"];
+ public string BeProud => this["BeProud"];
+ public string TasksAvoided => this["TasksAvoided"];
+ public string BreaksTaken => this["BreaksTaken"];
+ public string ExcusesGeneratedStat => this["ExcusesGeneratedStat"];
+ public string GamesPlayed => this["GamesPlayed"];
+ public string TotalClicks => this["TotalClicks"];
+ public string AIExcuseCalls => this["AIExcuseCalls"];
+ public string AchievementUnlocked => this["AchievementUnlocked"];
+
+ // Game strings
+ public string ClickMe => this["ClickMe"];
+ public string StartChallenge => this["StartChallenge"];
+ public string Wait => this["Wait"];
+ public string Start => this["Start"];
+ public string TapStartToBegin => this["TapStartToBegin"];
+ public string StartGame => this["StartGame"];
+ public string NewGame => this["NewGame"];
+ public string ThinkingOfNumber => this["ThinkingOfNumber"];
+ public string EnterGuess => this["EnterGuess"];
+ public string Guess => this["Guess"];
+ public string YourTurn => this["YourTurn"];
+ public string TiltToMove => this["TiltToMove"];
+ public string StartGame30s => this["StartGame30s"];
+ public string Generate => this["Generate"];
+
+ // Excuse engine settings
+ public string ExcuseEngine => this["ExcuseEngine"];
+ public string ExcuseEngineDesc => this["ExcuseEngineDesc"];
+ public string RandomGenerator => this["RandomGenerator"];
+ public string CloudAI => this["CloudAI"];
+ public string ApiEndpoint => this["ApiEndpoint"];
+ public string ApiEndpointPlaceholder => this["ApiEndpointPlaceholder"];
+ public string AiModel => this["AiModel"];
+ public string Generating => this["Generating"];
+ public string GroqApiKeyLabel => this["GroqApiKeyLabel"];
+ public string GroqApiKeyPlaceholder => this["GroqApiKeyPlaceholder"];
+ public string GroqGetKeyHint => this["GroqGetKeyHint"];
+ public string OnDeviceAI => this["OnDeviceAI"];
+ public string OnDeviceAIHint => this["OnDeviceAIHint"];
+ public string OnDeviceAIAvailable => this["OnDeviceAIAvailable"];
+ public string OnDeviceAIUnavailable => this["OnDeviceAIUnavailable"];
+
+ // About section
+ public string About => this["About"];
+ public string AboutDescription => this["AboutDescription"];
+ public string Author => this["Author"];
+}
diff --git a/10.0/Apps/Procrastinate/src/Services/ClickTracking.cs b/10.0/Apps/Procrastinate/src/Services/ClickTracking.cs
new file mode 100644
index 000000000..7a84c0136
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Services/ClickTracking.cs
@@ -0,0 +1,39 @@
+namespace procrastinate.Services;
+
+///
+/// Attached property to enable click tracking on buttons.
+/// Usage in XAML: services:ClickTracking.IsEnabled="True"
+///
+public static class ClickTracking
+{
+ public static readonly BindableProperty IsEnabledProperty =
+ BindableProperty.CreateAttached(
+ "IsEnabled",
+ typeof(bool),
+ typeof(ClickTracking),
+ false,
+ propertyChanged: OnIsEnabledChanged);
+
+ public static bool GetIsEnabled(BindableObject view) => (bool)view.GetValue(IsEnabledProperty);
+ public static void SetIsEnabled(BindableObject view, bool value) => view.SetValue(IsEnabledProperty, value);
+
+ private static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (bindable is Button button)
+ {
+ if ((bool)newValue)
+ {
+ button.Clicked += OnButtonClicked;
+ }
+ else
+ {
+ button.Clicked -= OnButtonClicked;
+ }
+ }
+ }
+
+ private static void OnButtonClicked(object? sender, EventArgs e)
+ {
+ StatsService.Instance?.IncrementClicks();
+ }
+}
diff --git a/10.0/Apps/Procrastinate/src/Services/ClickTrackingBehavior.cs b/10.0/Apps/Procrastinate/src/Services/ClickTrackingBehavior.cs
new file mode 100644
index 000000000..fcec7c54d
--- /dev/null
+++ b/10.0/Apps/Procrastinate/src/Services/ClickTrackingBehavior.cs
@@ -0,0 +1,21 @@
+namespace procrastinate.Services;
+
+public class ClickTrackingBehavior : Behavior