Social bar position is a click-toggle, not a drag — the drag-snap misbehaved for shaky hands (snapped up but wouldn't come back down; jitter around the deadzone). Clicking the bar now flips it top/bottom via ToggleSocialBarPosition; SocialBarSnap and its units removed — 153 tests passing, 0 warnings

This commit is contained in:
2026-08-13 11:04:32 -07:00
parent ac60a26d82
commit ea250c02a6
8 changed files with 58 additions and 141 deletions
+17 -28
View File
@@ -7,30 +7,19 @@
## Session state (last updated: 2026-08-13) ## Session state (last updated: 2026-08-13)
- **Branch:** `main`. TASK 4 **ship step 5.5 is implemented, tested, and staged for - **Branch:** `main`. TASK 4 **ship step 5.5** is **committed and pushed** (`ac60a26`,
commit** — the social bar bug-fix branch (drag snap + fediverse heal + the bar on "TASK 4 ship step 5.5: social bar bug fixes + bar on the live output").
the live output). Uncommitted: the feature work, tests (+8 → 155), and the memory - **This session (follow-up):** the drag-snap from 5.5 failed in practice — it snapped
updates in TASKS.md / ai.md / Services/index.md / this file. Next action is up but wouldn't come back down (jitter around the deadzone, per the user). **Superseded
`git add` + commit + push (feature + its memory docs in ONE commit, per the by a click-toggle** (user decision, KISS): `MainWindow.SocialBar_MouseLeftButtonDown`
working rules). `MainViewModel.ToggleSocialBarPosition()` flips the bar top ⇄ bottom; the bar rides
- **Finished this session:** both reported bugs diagnosed to root cause and fixed; `{Binding SocialBarTop}` alone; `SocialBarSnap` + its 2 tests removed. Build 0 warnings,
compositor rendering of the social bar added per user approval. **153 tests passing** (155 2 snap units). **Uncommitted:** this click-toggle change +
- **Bug 1 — drag didn't snap:** `Canvas.SetTop` set a local value that permanently its memory corrections (TASKS.md / ai.md / HANDOFF). Next action: commit + push.
overrides `{Binding SocialBarTop}`; the release-time snap could never win. Fixed - **Verified:** 0 warnings; 153/153 tests pass.
with `SocialBarSnap.Decide` (direction-snap during drag, ±6px deadzone) +
`bar.ClearValue(Canvas.TopProperty)` on release.
- **Bug 2 — Mastodon showed the generic honeycomb:** `@gramps@llamachile.tube`
had `Software = NULL` (nodeinfo only ever asked of the landing-page domain).
Fixed via subdomain probing (`mastodon.``social.` → … candidates, ~15s
budget), a load-time heal (`MainViewModel.HealFediverseSoftwareAsync`), and a
settable `SocialEntry.FediverseSoftware` that raises `LogoData`.
- **Compositor bar:** `Services/Compositor/SocialBarRenderer.cs` (new, WPF
RenderTargetBitmap → straight-alpha strip), `SceneCompositor` blits it above
the flash, `FramePump` gains a re-read-each-frame `socialBar:` seam,
`MainViewModel` owns the strip frame.
- **Verified:** Windows-host build **0 warnings**; **155/155 tests passing**
(147 baseline + 8 new). Full commit contents pending.
- **Landmines:** - **Landmines:**
- Never set a local `Canvas.SetTop` on the social bar — a local value permanently
overrides `{Binding SocialBarTop}` (the `ClearValue` lesson from 5.5).
- The pump reads the active scene on a background thread while the UI can still - The pump reads the active scene on a background thread while the UI can still
edit it — a concurrent-mutation exception is contained (logged + `Failed` + edit it — a concurrent-mutation exception is contained (logged + `Failed` +
the pump stops), not a crash. The background thread + video pipeline is the the pump stops), not a crash. The background thread + video pipeline is the
@@ -49,11 +38,11 @@
integration test (a real `MainWindow`), which never goes live — keep it that way. integration test (a real `MainWindow`), which never goes live — keep it that way.
- Sandbox can't reach outbound HTTPS — the subdomain-probe logic is verified via - Sandbox can't reach outbound HTTPS — the subdomain-probe logic is verified via
stub-handler tests only, not against the real `mastodon.llamachile.tube`. stub-handler tests only, not against the real `mastodon.llamachile.tube`.
- **Next step:** commit + push ship step 5.5 (one commit incl. memory docs). Then - **Next step:** commit + push the click-toggle follow-up. Then TASK 4 ship step 6 —
TASK 4 ship step 6 — health stats: bind `FramePump.HealthUpdated` health stats: bind `FramePump.HealthUpdated` (bitrate/FPS/duration) into the bottom
(bitrate/FPS/duration) into the bottom bar. Nothing else queued — do not expand bar. Nothing else queued — do not expand the task queue on your own. Optional, not
the task queue on your own. Optional, not queued: rewriting the healed entry's queued: rewriting the healed entry's `ProfileUrl` to
`ProfileUrl` to `https://mastodon.llamachile.tube/@gramps` (user must say the word). `https://mastodon.llamachile.tube/@gramps` (user must say the word).
- **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`; - **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`;
OAuth session token in `Helpers/TokenStore.cs` (DPAPI → `%APPDATA%\ytLlive\ytLlive.auth`); OAuth session token in `Helpers/TokenStore.cs` (DPAPI → `%APPDATA%\ytLlive\ytLlive.auth`);
+3 -4
View File
@@ -654,13 +654,12 @@
<!-- Social bar: one centered horizontal row at the top or bottom of <!-- Social bar: one centered horizontal row at the top or bottom of
the frame, green glow while enabled. Global resource — never a the frame, green glow while enabled. Global resource — never a
Source, no Sources entry. Dragging snaps to the nearest edge. --> Source, no Sources entry. Click toggles top ⇄ bottom. -->
<Grid x:Name="SocialBarElement" Width="1920" Canvas.Top="{Binding SocialBarTop}" <Grid x:Name="SocialBarElement" Width="1920" Canvas.Top="{Binding SocialBarTop}"
Background="Transparent" Background="Transparent"
Visibility="{Binding SocialBarVisible, Converter={StaticResource BoolToVis}}" Visibility="{Binding SocialBarVisible, Converter={StaticResource BoolToVis}}"
PreviewMouseLeftButtonDown="SocialBar_PreviewMouseLeftButtonDown" MouseLeftButtonDown="SocialBar_MouseLeftButtonDown"
PreviewMouseMove="SocialBar_PreviewMouseMove" Cursor="Hand">
PreviewMouseLeftButtonUp="SocialBar_PreviewMouseLeftButtonUp">
<Grid.Effect> <Grid.Effect>
<DropShadowEffect Color="{Binding SocialBarGlowBrush.Color}" <DropShadowEffect Color="{Binding SocialBarGlowBrush.Color}"
BlurRadius="18" ShadowDepth="0" Opacity="0.9"/> BlurRadius="18" ShadowDepth="0" Opacity="0.9"/>
+5 -53
View File
@@ -254,60 +254,12 @@ public partial class MainWindow : Window
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%"; => OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
// ─── Social bar drag: vertical only, direction-snaps to the top/bottom edge ─── // ─── Social bar: click toggles top ⇄ bottom. KISS — the drag variant was
private const double SocialBarBottomTop = 1040; // unusable for shaky hands (jitter around the direction deadzone), so the bar
private bool _isDraggingSocialBar; // rides on {Binding SocialBarTop} alone and a click flips it. ───
private double _socialBarDragStartY; private void SocialBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
private SocialBarPosition? _socialBarDragDirection;
private void SocialBar_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
var bar = (FrameworkElement)sender; _viewModel.ToggleSocialBarPosition();
_isDraggingSocialBar = true;
_socialBarDragDirection = null;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
_socialBarDragStartY = p.Y;
bar.CaptureMouse();
e.Handled = true;
}
private void SocialBar_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
// Derive the drag direction once it commits (past the deadzone) and keep it
// for the rest of the drag — the bar snaps to the edge it's being dragged
// toward and is never left mid-screen.
var direction = _socialBarDragDirection
?? SocialBarSnap.Decide(p.Y - _socialBarDragStartY);
if (direction != null)
{
_socialBarDragDirection = direction;
Canvas.SetTop(bar, direction == SocialBarPosition.Top ? 0 : SocialBarBottomTop);
}
e.Handled = true;
}
private void SocialBar_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
bar.ReleaseMouseCapture();
_isDraggingSocialBar = false;
var direction = _socialBarDragDirection
?? (Canvas.GetTop(bar) <= SocialBarBottomTop / 2.0
? SocialBarPosition.Top : SocialBarPosition.Bottom);
// ClearValue removes the local value Canvas.SetTop applied during the drag —
// a local value permanently overrides the {Binding SocialBarTop}, which is
// why the release snap never used to show. Clearing re-engages the binding.
bar.ClearValue(Canvas.TopProperty);
_viewModel.SetSocialBarPosition(direction);
e.Handled = true; e.Handled = true;
} }
-16
View File
@@ -263,19 +263,3 @@ public static class SocialServiceIcons
return true; return true;
} }
} }
/// <summary>
/// Direction-based snap for the social bar drag (preview): a drag toward the top
/// edge snaps the bar to the top, toward the bottom snaps it to the bottom — the
/// bar is never left mid-screen. Returns null inside the deadzone so a drag that
/// hasn't committed to a direction doesn't flip the bar.
/// </summary>
public static class SocialBarSnap
{
public static SocialBarPosition? Decide(double deltaY, double deadzone = 6)
{
if (deltaY <= -deadzone) return SocialBarPosition.Top;
if (deltaY >= deadzone) return SocialBarPosition.Bottom;
return null;
}
}
+18 -13
View File
@@ -477,12 +477,17 @@ exception is contained (logged + `Failed` + pump stops) rather than crashing.
#### Ship step 5.5 — Social bar bug fixes + the bar on the live output (2026-08-13) #### Ship step 5.5 — Social bar bug fixes + the bar on the live output (2026-08-13)
**Bug 1 — drag doesn't snap (root cause found):** `SocialBar_PreviewMouseMove` set `Canvas.SetTop(bar, …)` **Bug 1 — bar wouldn't reliably change position (root cause found, then simplified):** the original
with a local value, which permanently overrides the `Canvas.Top="{Binding SocialBarTop}"` binding — the drag set `Canvas.SetTop(bar, …)` with a local value, which permanently overrides the
release-time `SetSocialBarPosition` → `PropertyChanged(SocialBarTop)` could never beat it, so the bar stayed `Canvas.Top="{Binding SocialBarTop}"` binding — the release-time `SetSocialBarPosition` →
wherever it was dropped. Fixed by direction-snapping **during** the drag (`SocialBarSnap.Decide` — deadzone `PropertyChanged(SocialBarTop)` could never beat it. First fix added direction-snapping during the drag
±6px, once the direction commits the bar rides the edge it's dragged toward, never mid-screen) and on release (`SocialBarSnap.Decide`, ±6px deadzone) + `bar.ClearValue(Canvas.TopProperty)` on release — but that
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`. still misbehaved for shaky hands (jitter around the deadzone: it snapped up reliably, then refused to
come back down and snapped back to top). **Superseded by a click-toggle (KISS, user decision):** clicking
the bar in the preview flips it top ⇄ bottom (`MainViewModel.ToggleSocialBarPosition` → the existing
`SetSocialBarPosition`), the bar rides `{Binding SocialBarTop}` alone (no local values, no deadzone, no
jitter sensitivity), and `SocialBarSnap` was removed. The `ClearValue` lesson stands: never set a local
value on a property the binding owns.
**Bug 2 — Mastodon showed the generic 7-star honeycomb (root cause found):** the DB row **Bug 2 — Mastodon showed the generic 7-star honeycomb (root cause found):** the DB row
`@gramps@llamachile.tube` had `Software = NULL` — nodeinfo was only ever resolved against the identity domain `@gramps@llamachile.tube` had `Software = NULL` — nodeinfo was only ever resolved against the identity domain
@@ -505,13 +510,13 @@ BGRA strip (1920-wide, 40px content + 24px glow pad, green `#2ecc71` glow baked
`SourceRectHeight bar height`. `MainViewModel` owns the frame (`RenderSocialBarFrame`, re-rendered on `SourceRectHeight bar height`. `MainViewModel` owns the frame (`RenderSocialBarFrame`, re-rendered on
load/save/notify) and feeds the seam. load/save/notify) and feeds the seam.
**Tests (+8 → 155 passing, 0 warnings):** `SocialBarSnap.Decide` units (direction + deadzone + custom **Tests (+6 → 153 passing, 0 warnings):** settable `FediverseSoftware` updates `LogoData`,
deadzone), settable `FediverseSoftware` updates `LogoData`, subdomain-probe unit + null-when-silent unit, subdomain-probe unit + null-when-silent unit, the branch's **one integration test**
the branch's **one integration test** `Socials_HealMissingFediverseSoftware_RoundTripsThroughDb` `Socials_HealMissingFediverseSoftware_RoundTripsThroughDb` (temp-DB roundtrip: NULL software → healed via
(temp-DB roundtrip: NULL software → healed via the validator → persisted), compositor bar overlay the validator → persisted), compositor bar overlay (top/bottom + above-flash), `FramePump` bar pass-through
(top/bottom + above-flash), `FramePump` bar pass-through (Top then flipped to Bottom mid-run — the seam is (Top then flipped to Bottom mid-run — the seam is re-read each frame), and both `ISocialValidator` fakes
re-read each frame), and both `ISocialValidator` fakes (`FakeValidator`/`BlockingValidator`) gained (`FakeValidator`/`BlockingValidator`) gained `ResolveFediverseSoftwareAsync`. The drag-snap units
`ResolveFediverseSoftwareAsync`. (`SocialBarSnap`) were removed with the click-toggle supersession.
**Not included (say the word):** rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps`. **Not included (say the word):** rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps`.
+7 -1
View File
@@ -2013,7 +2013,7 @@ public class MainViewModel : ViewModelBase
ScheduleSave(); ScheduleSave();
} }
/// <summary>Drag release snaps the bar to the closest edge; called by the preview drag.</summary> /// <summary>Sets the bar's edge; called by the preview click-toggle.</summary>
public void SetSocialBarPosition(SocialBarPosition position) public void SetSocialBarPosition(SocialBarPosition position)
{ {
if (_socials == null || _socials.BarPosition == position) return; if (_socials == null || _socials.BarPosition == position) return;
@@ -2022,6 +2022,12 @@ public class MainViewModel : ViewModelBase
ScheduleSave(); ScheduleSave();
} }
/// <summary>Preview click flips the bar top ⇄ bottom (KISS — no drag math).</summary>
public void ToggleSocialBarPosition()
=> SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
? SocialBarPosition.Bottom
: SocialBarPosition.Top);
/// <summary>Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).</summary> /// <summary>Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).</summary>
private async Task SignOutYouTubeAsync() private async Task SignOutYouTubeAsync()
{ {
+8 -7
View File
@@ -422,8 +422,8 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
### Social bar (TASK 14 — shipped 2026-08-12, plan in TASKS.md; bar bug-fix branch 2026-08-13) ### Social bar (TASK 14 — shipped 2026-08-12, plan in TASKS.md; bar bug-fix branch 2026-08-13)
A **global bar layer** (never a Source, no sources-list row) that sits over the bottom or top of the A **global bar layer** (never a Source, no sources-list row) that sits over the bottom or top of the
output and carries the creator's social links — content-sized, centered, GREEN glow when ON, top/bottom output and carries the creator's social links — content-sized, centered, GREEN glow when ON, position is a
snap-drag (default BOTTOM, persisted `SocialBarPosition`). `Models/Socials.cs`: `SocialService` enum **click-toggle top ⇄ bottom** (default BOTTOM, persisted `SocialBarPosition`). `Models/Socials.cs`: `SocialService` enum
(YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/ (YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/
Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/ Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/
`FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` + `BarEnabled`; `BarJustify` dropped, `FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` + `BarEnabled`; `BarJustify` dropped,
@@ -447,11 +447,12 @@ never touch slot state).
**Bar bug-fix branch (2026-08-13) — three changes:** **Bar bug-fix branch (2026-08-13) — three changes:**
1. **Drag now direction-snaps** — a local `Canvas.SetTop` value permanently overrides the 1. **Positioning is a click-toggle (KISS)** — the original drag set a local `Canvas.SetTop` value that
`{Binding SocialBarTop}` (a binding can never win over a local value), so the old drag left the bar permanently overrides the `{Binding SocialBarTop}` (a binding can never win over a local value), so the
wherever it was dropped. Now `SocialBarSnap.Decide` (pure, in `Models/Socials.cs`) commits a direction bar stayed wherever it was dropped. The first drag-snap fix (`SocialBarSnap.Decide` + `ClearValue` on
once the drag passes the ±6px deadzone and rides the edge it's dragged toward; on release release) still failed for shaky hands — jitter around the ±6px deadzone snapped the bar up but wouldn't
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`. let it come back down. **Superseded by the user's click-toggle:** clicking the bar flips it top ⇄ bottom
(`MainViewModel.ToggleSocialBarPosition`), the bar rides the binding alone, and `SocialBarSnap` is gone.
2. **Fediverse software self-heal** — the DB row `@gramps@llamachile.tube` had `Software = NULL` because 2. **Fediverse software self-heal** — the DB row `@gramps@llamachile.tube` had `Software = NULL` because
nodeinfo was only ever asked of the identity domain (a landing page; the real instance is nodeinfo was only ever asked of the identity domain (a landing page; the real instance is
`mastodon.llamachile.tube`). `HttpSocialValidator.ResolveFediverseSoftwareAsync` now **probes `mastodon.llamachile.tube`). `HttpSocialValidator.ResolveFediverseSoftwareAsync` now **probes
-19
View File
@@ -30,25 +30,6 @@ public class SocialBarTests
=> throw new System.NotSupportedException(); => throw new System.NotSupportedException();
} }
[Fact]
public void SocialBarSnap_Decide_DirectionAndDeadzone()
{
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-10));
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-6));
Assert.Equal(SocialBarPosition.Bottom, SocialBarSnap.Decide(10));
Assert.Equal(SocialBarPosition.Bottom, SocialBarSnap.Decide(6));
Assert.Null(SocialBarSnap.Decide(5));
Assert.Null(SocialBarSnap.Decide(0));
Assert.Null(SocialBarSnap.Decide(-5));
}
[Fact]
public void SocialBarSnap_Decide_HonorsCustomDeadzone()
{
Assert.Null(SocialBarSnap.Decide(10, 20));
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-21, 20));
}
[Fact] [Fact]
public void SocialEntry_FediverseSoftware_SettableUpdatesLogo() public void SocialEntry_FediverseSoftware_SettableUpdatesLogo()
{ {