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:
+17
-28
@@ -7,30 +7,19 @@
|
||||
|
||||
## Session state (last updated: 2026-08-13)
|
||||
|
||||
- **Branch:** `main`. TASK 4 **ship step 5.5 is implemented, tested, and staged for
|
||||
commit** — the social bar bug-fix branch (drag snap + fediverse heal + the bar on
|
||||
the live output). Uncommitted: the feature work, tests (+8 → 155), and the memory
|
||||
updates in TASKS.md / ai.md / Services/index.md / this file. Next action is
|
||||
`git add` + commit + push (feature + its memory docs in ONE commit, per the
|
||||
working rules).
|
||||
- **Finished this session:** both reported bugs diagnosed to root cause and fixed;
|
||||
compositor rendering of the social bar added per user approval.
|
||||
- **Bug 1 — drag didn't snap:** `Canvas.SetTop` set a local value that permanently
|
||||
overrides `{Binding SocialBarTop}`; the release-time snap could never win. Fixed
|
||||
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.
|
||||
- **Branch:** `main`. TASK 4 **ship step 5.5** is **committed and pushed** (`ac60a26`,
|
||||
"TASK 4 ship step 5.5: social bar bug fixes + bar on the live output").
|
||||
- **This session (follow-up):** the drag-snap from 5.5 failed in practice — it snapped
|
||||
up but wouldn't come back down (jitter around the deadzone, per the user). **Superseded
|
||||
by a click-toggle** (user decision, KISS): `MainWindow.SocialBar_MouseLeftButtonDown`
|
||||
→ `MainViewModel.ToggleSocialBarPosition()` flips the bar top ⇄ bottom; the bar rides
|
||||
`{Binding SocialBarTop}` alone; `SocialBarSnap` + its 2 tests removed. Build 0 warnings,
|
||||
**153 tests passing** (155 − 2 snap units). **Uncommitted:** this click-toggle change +
|
||||
its memory corrections (TASKS.md / ai.md / HANDOFF). Next action: commit + push.
|
||||
- **Verified:** 0 warnings; 153/153 tests pass.
|
||||
- **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
|
||||
edit it — a concurrent-mutation exception is contained (logged + `Failed` +
|
||||
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.
|
||||
- Sandbox can't reach outbound HTTPS — the subdomain-probe logic is verified via
|
||||
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
|
||||
TASK 4 ship step 6 — health stats: bind `FramePump.HealthUpdated`
|
||||
(bitrate/FPS/duration) into the bottom bar. Nothing else queued — do not expand
|
||||
the task queue on your own. Optional, not queued: rewriting the healed entry's
|
||||
`ProfileUrl` to `https://mastodon.llamachile.tube/@gramps` (user must say the word).
|
||||
- **Next step:** commit + push the click-toggle follow-up. Then TASK 4 ship step 6 —
|
||||
health stats: bind `FramePump.HealthUpdated` (bitrate/FPS/duration) into the bottom
|
||||
bar. Nothing else queued — do not expand the task queue on your own. Optional, not
|
||||
queued: rewriting the healed entry's `ProfileUrl` to
|
||||
`https://mastodon.llamachile.tube/@gramps` (user must say the word).
|
||||
|
||||
- **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`);
|
||||
|
||||
+3
-4
@@ -654,13 +654,12 @@
|
||||
|
||||
<!-- Social bar: one centered horizontal row at the top or bottom of
|
||||
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}"
|
||||
Background="Transparent"
|
||||
Visibility="{Binding SocialBarVisible, Converter={StaticResource BoolToVis}}"
|
||||
PreviewMouseLeftButtonDown="SocialBar_PreviewMouseLeftButtonDown"
|
||||
PreviewMouseMove="SocialBar_PreviewMouseMove"
|
||||
PreviewMouseLeftButtonUp="SocialBar_PreviewMouseLeftButtonUp">
|
||||
MouseLeftButtonDown="SocialBar_MouseLeftButtonDown"
|
||||
Cursor="Hand">
|
||||
<Grid.Effect>
|
||||
<DropShadowEffect Color="{Binding SocialBarGlowBrush.Color}"
|
||||
BlurRadius="18" ShadowDepth="0" Opacity="0.9"/>
|
||||
|
||||
+5
-53
@@ -254,60 +254,12 @@ public partial class MainWindow : Window
|
||||
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
|
||||
|
||||
// ─── Social bar drag: vertical only, direction-snaps to the top/bottom edge ───
|
||||
private const double SocialBarBottomTop = 1040;
|
||||
private bool _isDraggingSocialBar;
|
||||
private double _socialBarDragStartY;
|
||||
private SocialBarPosition? _socialBarDragDirection;
|
||||
|
||||
private void SocialBar_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
// ─── Social bar: click toggles top ⇄ bottom. KISS — the drag variant was
|
||||
// unusable for shaky hands (jitter around the direction deadzone), so the bar
|
||||
// rides on {Binding SocialBarTop} alone and a click flips it. ───
|
||||
private void SocialBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var bar = (FrameworkElement)sender;
|
||||
_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);
|
||||
_viewModel.ToggleSocialBarPosition();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -263,19 +263,3 @@ public static class SocialServiceIcons
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
**Bug 1 — drag doesn't snap (root cause found):** `SocialBar_PreviewMouseMove` set `Canvas.SetTop(bar, …)`
|
||||
with a local value, which permanently overrides the `Canvas.Top="{Binding SocialBarTop}"` binding — the
|
||||
release-time `SetSocialBarPosition` → `PropertyChanged(SocialBarTop)` could never beat it, so the bar stayed
|
||||
wherever it was dropped. Fixed by direction-snapping **during** the drag (`SocialBarSnap.Decide` — deadzone
|
||||
±6px, once the direction commits the bar rides the edge it's dragged toward, never mid-screen) and on release
|
||||
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`.
|
||||
**Bug 1 — bar wouldn't reliably change position (root cause found, then simplified):** the original
|
||||
drag set `Canvas.SetTop(bar, …)` with a local value, which permanently overrides the
|
||||
`Canvas.Top="{Binding SocialBarTop}"` binding — the release-time `SetSocialBarPosition` →
|
||||
`PropertyChanged(SocialBarTop)` could never beat it. First fix added direction-snapping during the drag
|
||||
(`SocialBarSnap.Decide`, ±6px deadzone) + `bar.ClearValue(Canvas.TopProperty)` on release — but that
|
||||
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
|
||||
`@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
|
||||
load/save/notify) and feeds the seam.
|
||||
|
||||
**Tests (+8 → 155 passing, 0 warnings):** `SocialBarSnap.Decide` units (direction + deadzone + custom
|
||||
deadzone), settable `FediverseSoftware` updates `LogoData`, subdomain-probe unit + null-when-silent unit,
|
||||
the branch's **one integration test** `Socials_HealMissingFediverseSoftware_RoundTripsThroughDb`
|
||||
(temp-DB roundtrip: NULL software → healed via the validator → persisted), compositor bar overlay
|
||||
(top/bottom + above-flash), `FramePump` bar pass-through (Top then flipped to Bottom mid-run — the seam is
|
||||
re-read each frame), and both `ISocialValidator` fakes (`FakeValidator`/`BlockingValidator`) gained
|
||||
`ResolveFediverseSoftwareAsync`.
|
||||
**Tests (+6 → 153 passing, 0 warnings):** settable `FediverseSoftware` updates `LogoData`,
|
||||
subdomain-probe unit + null-when-silent unit, the branch's **one integration test**
|
||||
`Socials_HealMissingFediverseSoftware_RoundTripsThroughDb` (temp-DB roundtrip: NULL software → healed via
|
||||
the validator → persisted), compositor bar overlay (top/bottom + above-flash), `FramePump` bar pass-through
|
||||
(Top then flipped to Bottom mid-run — the seam is re-read each frame), and both `ISocialValidator` fakes
|
||||
(`FakeValidator`/`BlockingValidator`) gained `ResolveFediverseSoftwareAsync`. The drag-snap units
|
||||
(`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`.
|
||||
|
||||
|
||||
@@ -2013,7 +2013,7 @@ public class MainViewModel : ViewModelBase
|
||||
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)
|
||||
{
|
||||
if (_socials == null || _socials.BarPosition == position) return;
|
||||
@@ -2022,6 +2022,12 @@ public class MainViewModel : ViewModelBase
|
||||
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>
|
||||
private async Task SignOutYouTubeAsync()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
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
|
||||
snap-drag (default BOTTOM, persisted `SocialBarPosition`). `Models/Socials.cs`: `SocialService` enum
|
||||
output and carries the creator's social links — content-sized, centered, GREEN glow when ON, position is a
|
||||
**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/
|
||||
Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/
|
||||
`FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` + `BarEnabled`; `BarJustify` dropped,
|
||||
@@ -447,11 +447,12 @@ never touch slot state).
|
||||
|
||||
**Bar bug-fix branch (2026-08-13) — three changes:**
|
||||
|
||||
1. **Drag now direction-snaps** — a local `Canvas.SetTop` value permanently overrides the
|
||||
`{Binding SocialBarTop}` (a binding can never win over a local value), so the old drag left the bar
|
||||
wherever it was dropped. Now `SocialBarSnap.Decide` (pure, in `Models/Socials.cs`) commits a direction
|
||||
once the drag passes the ±6px deadzone and rides the edge it's dragged toward; on release
|
||||
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`.
|
||||
1. **Positioning is a click-toggle (KISS)** — the original drag set a local `Canvas.SetTop` value that
|
||||
permanently overrides the `{Binding SocialBarTop}` (a binding can never win over a local value), so the
|
||||
bar stayed wherever it was dropped. The first drag-snap fix (`SocialBarSnap.Decide` + `ClearValue` on
|
||||
release) still failed for shaky hands — jitter around the ±6px deadzone snapped the bar up but wouldn't
|
||||
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
|
||||
nodeinfo was only ever asked of the identity domain (a landing page; the real instance is
|
||||
`mastodon.llamachile.tube`). `HttpSocialValidator.ResolveFediverseSoftwareAsync` now **probes
|
||||
|
||||
@@ -30,25 +30,6 @@ public class SocialBarTests
|
||||
=> 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]
|
||||
public void SocialEntry_FediverseSoftware_SettableUpdatesLogo()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user