Skip to content
Draft
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
3 changes: 3 additions & 0 deletions MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public static MauiApp CreateMauiApp()
{
fonts.AddFont("Hack-Regular.ttf", "HackRegular");
fonts.AddFont("Hack-Bold.ttf", "HackBold");
// Subsetted Material Design Icons (microphone glyph U+F036C only, ~840 bytes) for
// the dictation cue (#14) — not the full ~1.3 MB MDI webfont.
fonts.AddFont("mdi-microphone.ttf", "MaterialIcons");
});

#if DEBUG
Expand Down
Binary file added Resources/Fonts/mdi-microphone.ttf
Binary file not shown.
33 changes: 33 additions & 0 deletions Views/EditorPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
nav bar, shown only on Android (Shell.NavBarIsVisible above); on Windows the nav bar is
hidden so they don't appear, and the bottom bar + MenuBar stay canonical. -->
<ContentPage.ToolbarItems>
<!-- Dictation cue (#14): a visible mic icon in the Android app bar. Dictation is the OS's own
feature (the keyboard mic); tapping focuses the editor + shows a transient how-to hint. -->
<ToolbarItem Order="Primary" Clicked="OnDictateClicked" Text="Dictate"
SemanticProperties.Description="Dictate: focus the editor and show how to use your keyboard's mic">
<ToolbarItem.IconImageSource>
<FontImageSource FontFamily="MaterialIcons" Glyph="&#xF036C;" Color="{DynamicResource OnSurface}" Size="22" />
</ToolbarItem.IconImageSource>
</ToolbarItem>
<ToolbarItem Text="New post" Command="{Binding NewPostCommand}" Order="Secondary" Priority="0" />
<ToolbarItem Text="Open post" Command="{Binding OpenPostCommand}" Order="Secondary" Priority="1" />
<ToolbarItem Text="Save" Command="{Binding SaveCommand}" Order="Secondary" Priority="2" />
Expand Down Expand Up @@ -109,6 +117,18 @@
<Label Text="preview" VerticalOptions="Center" FontFamily="HackRegular" FontSize="13"
TextColor="{DynamicResource OnSurface}" />
</HorizontalStackLayout>
<!-- Dictation cue (#14): tap to focus the editor + show the Win+H hint. mdi-microphone
path (theme-reactive Fill), matching the preview/sync icon convention. -->
<HorizontalStackLayout Margin="6,0" VerticalOptions="Center"
ToolTipProperties.Text="Dictate (Win+H)"
SemanticProperties.Description="Dictate: focus the editor and show how to use Windows dictation">
<HorizontalStackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDictateTapped" />
</HorizontalStackLayout.GestureRecognizers>
<shapes:Path Aspect="Uniform" HeightRequest="16" WidthRequest="16" VerticalOptions="Center"
Fill="{DynamicResource OnSurface}"
Data="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" />
</HorizontalStackLayout>
<Button Text="about" Command="{Binding AboutCommand}"
Style="{StaticResource StatusBarButton}"
SemanticProperties.Description="About PostXING" />
Expand Down Expand Up @@ -197,5 +217,18 @@
</Grid>
</Border>
</Grid>

<!-- Dictation how-to hint (#14): a transient toast the mic cue fades in, then auto-dismisses.
Self-contained (no CommunityToolkit dependency); InputTransparent so it never blocks taps. -->
<Border x:Name="DictationHint" IsVisible="False" Opacity="0"
BackgroundColor="{StaticResource Ink850}"
Padding="14,8" Margin="0,0,0,48"
HorizontalOptions="Center" VerticalOptions="End"
StrokeThickness="0"
StrokeShape="RoundRectangle 6,6,6,6"
InputTransparent="True">
<Label x:Name="DictationHintLabel" FontFamily="HackRegular" FontSize="13"
TextColor="{StaticResource Paper100}" />
</Border>
</Grid>
</ContentPage>
34 changes: 34 additions & 0 deletions Views/EditorPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text;
using System.Text.Json;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Storage;
using PostXING.App.Services;
using PostXING.ViewModels;
Expand Down Expand Up @@ -264,6 +265,39 @@ private async void OnSyncChipTapped(object? sender, TappedEventArgs e)
return string.IsNullOrWhiteSpace(msg) ? null : msg;
}

// Dictation cue (#14): PostXING has no speech engine — dictation is the OS's own feature
// (Windows Win+H, the Android keyboard mic). The mic icon focuses the editor so dictation
// lands in it, then shows a transient how-to hint. Both the Windows status-bar Path (Tapped)
// and the Android toolbar item (Clicked) route here.
private void OnDictateTapped(object? sender, TappedEventArgs e) => _ = ShowDictationHintAsync();
private void OnDictateClicked(object? sender, EventArgs e) => _ = ShowDictationHintAsync();

private bool _dictationHintBusy;
private async Task ShowDictationHintAsync()
{
if (_dictationHintBusy) return;
_dictationHintBusy = true;
try
{
// Focus the editor via the existing JS hook so dictation deposits text into it.
// Fire-and-forget: EvaluateJavaScriptAsync can hang on Android, but the JS still runs.
try { _ = EditorWebView.EvaluateJavaScriptAsync("if(window.PostXING){window.PostXING.focus()}"); }
catch { /* focus is best-effort */ }

var platform = DeviceInfo.Platform == DevicePlatform.WinUI ? DictationPlatform.Windows
: DeviceInfo.Platform == DevicePlatform.Android ? DictationPlatform.Android
: DictationPlatform.Other;
DictationHintLabel.Text = DictationHints.For(platform);

DictationHint.IsVisible = true;
await DictationHint.FadeToAsync(1, 150);
await Task.Delay(2500);
await DictationHint.FadeToAsync(0, 250);
DictationHint.IsVisible = false;
}
finally { _dictationHintBusy = false; }
}

#if ANDROID
// The JS->host bridge is dead on Android, so a keystroke can't push a 'dirty' signal the way
// it does on desktop (RawMessageReceived "change"). Instead, poll the editor text on a timer
Expand Down
27 changes: 27 additions & 0 deletions docs/dictation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dictating posts in PostXING

PostXING doesn't ship its own speech engine — it uses your operating system's built-in dictation, which is already fast, private (recognition runs on-device), and supports punctuation/formatting commands. The mic button in the editor is a **reminder and focus shortcut**: tap it to focus the editor, then start your OS dictation.

## Windows

1. Click into the editor (or tap the **mic** button in the status bar to focus it).
2. Press **`Win + H`** to open the Windows dictation toolbar.
3. Speak. Windows types the transcription straight into the editor.
4. Use voice commands for punctuation and structure — e.g. *"period"*, *"comma"*, *"new line"*, *"new paragraph"*, *"open quote" / "close quote"*.
5. Say *"stop dictation"* or press `Win + H` again to stop.

The dictation toolbar floats at the top of the screen and stays out of the editor, so you can dictate without any in-app chrome.

## Android

1. Tap into the editor (or tap the **mic** button in the top app bar to focus it).
2. On your soft keyboard, tap its **microphone** key (most Android keyboards — Gboard, etc. — put it near the space bar or top row).
3. Speak. The keyboard inserts the transcription.

PostXING's editor is written to handle voice input cleanly — it won't drop your words mid-sentence while it re-colorizes the syntax (it suppresses the highlight pass during IME composition and re-runs it once your phrase is committed).

## Notes

- **Nothing extra leaves your device.** Dictation is the OS's own recognition; PostXING only receives the text the keyboard/toolbar types, exactly as if you'd typed it.
- The mic button does **not** record or transcribe itself — it focuses the editor and reminds you of the shortcut. The actual dictation session is owned by Windows / your Android keyboard.
- If the mic key isn't on your Android keyboard, enable voice typing in your keyboard's settings (Gboard → Settings → Voice typing).
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Dictation discoverability — design

**Issue:** [#14 "Enable dictation"](https://github.com/BlueFenixProductions/PostXING/issues/14)
**Date:** 2026-06-10
**Status:** Approved design, pending implementation.

## Problem

Users want to dictate posts. Dictation **already works** via the OS — Android's keyboard mic (the editor's IME composition path in `index.html` is explicitly written to support it) and Windows `Win+H` dictate into any focused field, including the editor's contenteditable. What's missing is **discoverability**: there's no in-app cue, so users don't know they can dictate. The issue author leaned toward a docs how-to plus an `mdi-mic` cue rather than building a speech engine.

## Decisions (from the brainstorming interview)

1. **Deliver a docs how-to + a subtle in-app mic cue.** No speech-recognition engine. (Operator decision.)
2. **The cue focuses the editor and shows a transient, auto-dismissing hint** pointing at the OS dictation — no OS input injection. (Operator decision.)
3. **Use a real `mdi-microphone` icon** (a Material Design Icons font), not a text affordance — "the icon is visually useful to humans." (Operator decision; overrides the text-label house aesthetic for this one affordance.)
4. **Show the cue on both platforms** (not Windows-only). (Operator decision.)

## Design

### Material icon font

Register a Material Design Icons TTF in `Resources/Fonts/` (alongside Hack) via `MauiProgram.ConfigureFonts`, e.g. `fonts.AddFont("materialdesignicons-webfont.ttf", "MaterialIcons")`. **Subset the font to the few glyphs used** (`microphone`, and `microphone-off` for a possible future toggle) to avoid shipping the full ~1.2 MB MDI webfont in the APK/MSIX — a subsetted TTF is a few KB. The mic glyph is referenced via a `FontImageSource` with `FontFamily="MaterialIcons"` and the `mdi-microphone` codepoint **U+F036C** -- above the BMP, so C# uses the surrogate pair `"\U000F036C"` and XAML uses the `&#xF036C;` entity. Some MDI builds remap glyphs into the BMP private-use range; if the bundled font does, use that codepoint instead.

### Cue placement

- **Windows** — a mic `Button` in the editor's bottom status bar (`EditorPage.xaml`, `Grid.Row=1`), using `ImageSource` = the `FontImageSource` mic glyph instead of `Text`. Sits with `new`/`open`/`settings`/…; the icon is the one non-text affordance, intentionally.
- **Android** — a **primary** `ToolbarItem` (visible in the Material top app bar, not the overflow) with `IconImageSource` = the same `FontImageSource`. The existing actions stay in the overflow (`Order="Secondary"`); the mic is promoted so it's a visible, tappable icon.

### Behavior

Tapping the mic:
1. **Focuses the editor** — `EditorWebView.EvaluateJavaScriptAsync("window.PostXING.focus()")` -- `window.PostXING.focus()` already exists in `index.html` (calls `editor.focus()`), so no JS change is needed. (Fire-and-forget on Android per the existing bridge pattern.)
2. **Shows a transient, auto-dismissing hint** with platform-specific text:
- Windows: `Press Win+H to dictate`
- Android: `Tap the mic on your keyboard to dictate`

The hint is a self-contained element (a `Border`+`Label` overlaid near the status bar) that fades in, waits ~2.5 s, and fades out — **no new dependency** (CommunityToolkit.Maui is not referenced). A single `DictationHintRequested` event / bool drives its visibility.

### Hint text helper (the testable seam)

The platform→hint-text mapping lives in a tiny pure helper (e.g. `DictationHints.For(DevicePlatform)` returning the string), so the one piece of logic is unit-testable off the MAUI TFM in `PostXING.ViewModels`. The focus call and the fade are view-layer (`EditorPage` code-behind), not unit-tested.

### Docs

`docs/dictation.md` — "Dictating posts in PostXING":
- **Windows:** press `Win+H` to open the dictation toolbar; speak; it types into the editor. Punctuation commands ("period", "new line"). The in-app mic button is a reminder/focus shortcut, not a separate engine.
- **Android:** tap the mic on your soft keyboard; the editor handles voice input (it's written to not drop composition mid-highlight).
- Note that this uses the OS's own speech recognition — nothing leaves the device beyond what the OS dictation already does.

## Acceptance criteria

- A mic icon (real `mdi-microphone` glyph) is visible in the editor on both Windows and Android.
- Tapping it focuses the editor and briefly shows the correct platform hint, then auto-dismisses.
- `docs/dictation.md` documents Win+H (Windows) and the keyboard mic (Android).
- The Material icon font is subsetted (not the full ~1.2 MB webfont).
- No speech-recognition code, no `Win+H` injection; existing editor behavior unchanged.

## Non-goals (YAGNI)

- No in-app speech-to-text engine (`System.Speech` / `Windows.Media.SpeechRecognition`).
- No programmatic `Win+H` injection.
- No mic-state/recording UI — the OS owns the dictation session.
23 changes: 23 additions & 0 deletions src/PostXING.ViewModels/DictationHints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace PostXING.ViewModels;

/// <summary>Which platform's built-in dictation the cue points at (#14).</summary>
public enum DictationPlatform { Windows, Android, Other }

/// <summary>
/// The transient hint the dictation mic cue shows — platform-specific, since dictation is the OS's
/// own feature (Windows Win+H, the Android keyboard mic). Pure and MAUI-free so the one piece of
/// logic is unit-testable off the MAUI TFM; the page maps the running platform and calls <see cref="For"/>.
/// </summary>
public static class DictationHints
{
public const string Windows = "Press Win+H to dictate";
public const string Android = "Tap the mic on your keyboard to dictate";
public const string Default = "Use your device's voice dictation";

public static string For(DictationPlatform platform) => platform switch
{
DictationPlatform.Windows => Windows,
DictationPlatform.Android => Android,
_ => Default,
};
}
23 changes: 23 additions & 0 deletions tests/PostXING.ViewModels.Tests/DictationHintsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using PostXING.ViewModels;
using Shouldly;
using Xunit;

namespace PostXING.ViewModels.Tests;

public sealed class DictationHintsTests
{
[Theory]
[InlineData(DictationPlatform.Windows, "Win+H")]
[InlineData(DictationPlatform.Android, "keyboard")]
public void For_returns_the_platform_specific_hint(DictationPlatform platform, string fragment)
{
DictationHints.For(platform).ShouldContain(fragment);
}

[Fact]
public void For_unknown_platform_falls_back_to_a_generic_hint()
{
DictationHints.For(DictationPlatform.Other).ShouldBe(DictationHints.Default);
DictationHints.For(DictationPlatform.Other).ShouldNotBeNullOrWhiteSpace();
}
}