Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions 10.0/Apps/Procrastinate/README.md
Original file line number Diff line number Diff line change
@@ -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.

![Procrastinate App Screenshots](images/screenshots.png)

## 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)
Binary file added 10.0/Apps/Procrastinate/images/screenshots.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions 10.0/Apps/Procrastinate/src/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:procrastinate"
x:Class="procrastinate.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
31 changes: 31 additions & 0 deletions 10.0/Apps/Procrastinate/src/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
75 changes: 75 additions & 0 deletions 10.0/Apps/Procrastinate/src/AppShell.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="procrastinate.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:procrastinate"
xmlns:pages="clr-namespace:procrastinate.Pages"
xmlns:strings="clr-namespace:procrastinate.Services"
x:Name="ThisShell"
Title="Procrastinator's Helper">

<Shell.Resources>
<ResourceDictionary>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Nord6}, Dark={StaticResource Nord0}}"/>
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Nord0}, Dark={StaticResource Nord6}}"/>
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Nord0}, Dark={StaticResource Nord6}}"/>
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource Nord5}, Dark={StaticResource Nord1}}"/>
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Nord1}, Dark={StaticResource Nord4}}"/>
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Nord1}, Dark={StaticResource Nord4}}"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="{StaticResource Nord3}"/>
<Setter Property="Shell.FlyoutBackgroundColor" Value="{AppThemeBinding Light={StaticResource Nord5}, Dark={StaticResource Nord1}}"/>
</Style>
</ResourceDictionary>
</Shell.Resources>

<!-- iOS: Bottom TabBar -->
<TabBar x:Name="MobileTabBar">
<ShellContent Title="{Binding TabTasks, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:TasksPage}" Route="TasksPage">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf46d;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabGames, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:GamesPage}" Route="GamesPage">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf11b;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabExcuses, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:ExcusePage}" Route="ExcusePage">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf630;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabStats, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:StatsPage}" Route="StatsPage">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf201;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
</TabBar>

<!-- Mac Catalyst: Flyout sidebar -->
<FlyoutItem x:Name="DesktopFlyout" FlyoutDisplayOptions="AsMultipleItems">
<ShellContent Title="{Binding TabTasks, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:TasksPage}" Route="TasksPageDesktop">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf46d;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabGames, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:GamesPage}" Route="GamesPageDesktop">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf11b;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabExcuses, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:ExcusePage}" Route="ExcusePageDesktop">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf630;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="{Binding TabStats, Source={x:Static strings:AppStrings.Instance}}" ContentTemplate="{DataTemplate pages:StatsPage}" Route="StatsPageDesktop">
<ShellContent.Icon>
<FontImageSource Glyph="&#xf201;" FontFamily="FontAwesomeSolid" Color="{StaticResource Nord4}"/>
</ShellContent.Icon>
</ShellContent>
</FlyoutItem>

</Shell>
77 changes: 77 additions & 0 deletions 10.0/Apps/Procrastinate/src/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
21 changes: 21 additions & 0 deletions 10.0/Apps/Procrastinate/src/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
69 changes: 69 additions & 0 deletions 10.0/Apps/Procrastinate/src/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -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<App>()
.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<IChatClient>(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<IEmbeddingGenerator<string, Embedding<float>>>(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<StatsService>();
builder.Services.AddSingleton<ExcuseService>();
builder.Services.AddSingleton<TicTacToeAI>();

// Pages
builder.Services.AddTransient<TasksPage>();
builder.Services.AddTransient<GamesPage>();
builder.Services.AddTransient<ExcusePage>();
builder.Services.AddTransient<StatsPage>();
builder.Services.AddTransient<SettingsPage>();

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}
Loading
Loading