diff --git a/HANDOFF.md b/HANDOFF.md index e499a15..b7a5543 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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`); diff --git a/MainWindow.xaml b/MainWindow.xaml index 960eae5..3f8103f 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -654,13 +654,12 @@ + Source, no Sources entry. Click toggles top ⇄ bottom. --> + MouseLeftButtonDown="SocialBar_MouseLeftButtonDown" + Cursor="Hand"> diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 5f4691c..6b86b6e 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -254,60 +254,12 @@ public partial class MainWindow : Window private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs 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; } diff --git a/Models/Socials.cs b/Models/Socials.cs index 890654a..1608bc5 100644 --- a/Models/Socials.cs +++ b/Models/Socials.cs @@ -263,19 +263,3 @@ public static class SocialServiceIcons return true; } } - -/// -/// 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. -/// -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; - } -} diff --git a/TASKS.md b/TASKS.md index 0e6298b..32c3beb 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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`. diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index d7b9bff..f945fcb 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -2013,7 +2013,7 @@ public class MainViewModel : ViewModelBase ScheduleSave(); } - /// Drag release snaps the bar to the closest edge; called by the preview drag. + /// Sets the bar's edge; called by the preview click-toggle. public void SetSocialBarPosition(SocialBarPosition position) { if (_socials == null || _socials.BarPosition == position) return; @@ -2022,6 +2022,12 @@ public class MainViewModel : ViewModelBase ScheduleSave(); } + /// Preview click flips the bar top ⇄ bottom (KISS — no drag math). + public void ToggleSocialBarPosition() + => SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top + ? SocialBarPosition.Bottom + : SocialBarPosition.Top); + /// Signs out of YouTube (the delete-the-YouTube-slot action in the dialog). private async Task SignOutYouTubeAsync() { diff --git a/ai.md b/ai.md index 86f558b..f33c505 100644 --- a/ai.md +++ b/ai.md @@ -422,8 +422,8 @@ seam:** `Func`, `Func` resolver, `Func 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() {