fix(border-color): keep ComboBox popup clicks inside LeftPanel (TASK 63)
This commit is contained in:
@@ -54,9 +54,12 @@ public partial class LeftPanel : UserControl
|
||||
{
|
||||
while (child != null && !ReferenceEquals(child, ancestor))
|
||||
{
|
||||
child = child is Visual or System.Windows.Media.Media3D.Visual3D
|
||||
var next = child is Visual or System.Windows.Media.Media3D.Visual3D
|
||||
? VisualTreeHelper.GetParent(child)
|
||||
: LogicalTreeHelper.GetParent(child);
|
||||
: null;
|
||||
child = next
|
||||
?? LogicalTreeHelper.GetParent(child)
|
||||
?? (child as FrameworkElement)?.TemplatedParent;
|
||||
}
|
||||
return child != null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
|
||||
| `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` |
|
||||
| `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot (TASK 57 Unit 2: the resize routes through `MainViewModel.ResizeWebcamInVerticalTier` — the Chat camera's two sizes, then the snap) |
|
||||
| `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight |
|
||||
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour |
|
||||
| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard |
|
||||
| `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here |
|
||||
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
|
||||
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
|
||||
|
||||
+17
-8
@@ -52,7 +52,8 @@
|
||||
(`SceneElement.BorderColorName`, empty for a custom hex), and the hex field keeps a fixed
|
||||
width so the name stays next to the hex. The field stays editable (a custom hex can
|
||||
still be typed). Guard: `ytLive.Tests/BorderColorNameTests.cs`.
|
||||
- **TASK 63 - the border-colour picker IS the app's drop-down** (shipped 2026-10-21). The creator:
|
||||
- **TASK 63 - the border-colour picker IS the app's drop-down** (shipped 2026-10-21; **popup click
|
||||
deselection fixed 2026-10-22**). The creator:
|
||||
*"why does the drop down under the web cam properties not show the same style for the border
|
||||
color drop down list box?"* and *"we established look-and-feel standards. And yet you keep
|
||||
ignoring these and I'm wondering why."* **Root cause of the miss:** the TASK 58 sweep looked for
|
||||
@@ -66,8 +67,17 @@
|
||||
`WebcamBorderColorPicker_Click` are **gone**. The closed box != the rows only through
|
||||
`Helpers/ClosedBoxTemplateSelector.cs` (WPF copies `ItemTemplate` into the read-only
|
||||
`SelectionBoxItemTemplate`; the selector is told them apart by the `ContentPresenter`'s
|
||||
templated parent - `ComboBox` = closed box, `ComboBoxItem` = a row). Guard:
|
||||
`ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real widget**). The TASK 60
|
||||
templated parent - `ComboBox` = closed box, `ComboBoxItem` = a row). **2026-10-22 creator bug fix:**
|
||||
clicking a colour in the open `ComboBox` popup registered as an exit from the properties listing
|
||||
because `LeftPanel.IsDescendantOf` used `child is Visual ? VisualTreeHelper.GetParent(child) :
|
||||
LogicalTreeHelper.GetParent(child)` — at `PopupRoot` (which IS a `Visual`),
|
||||
`VisualTreeHelper.GetParent` returns `null`, so `IsDescendantOf` stopped without crossing
|
||||
`PopupRoot`'s logical parent (`PART_Popup` → `ComboBox` → `ElementsScrollViewer`), and
|
||||
`MainWindow.Window_PreviewMouseLeftButtonDown` cleared `SelectedElement = null`. Fixed by falling
|
||||
back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent`
|
||||
returns `null`. Guard: `ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real
|
||||
widget**, including `LeftPanel.IsClickInside` on an open popup row and clicking a row to change
|
||||
`BorderColor`). The TASK 60
|
||||
guard (`BorderColorPaletteTests`) was updated to guard the new wiring (the drop-down is bound to
|
||||
the palette; no second list of colours anywhere).
|
||||
- **TASK 55 slice 1 — the log is capped (shipped 2026-10-22).** `startup.log` was uncapped (23 MB
|
||||
@@ -233,11 +243,10 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
the full top area and the circle option is not offered. **PASSES.**
|
||||
|
||||
## Next step
|
||||
- **Creator check of TASK 63 (needs the creator's eyes)** — open Live → Layers → webcam →
|
||||
Properties → Border Color: the drop-down must now LOOK like the Display/Font drop-downs (same
|
||||
dark themed chrome, same chevron), the CLOSED box must show the colour's name beside the hex
|
||||
field ("Crimson"), and the open list must show swatch + hex + name. The hex field still accepts a
|
||||
typed colour (the drop-down then shows the swatch with no name — "no name" is the honest answer).
|
||||
- **✅ TASK 63 + popup-click fix verified by creator (2026-10-22)** — clicking a colour in the
|
||||
Border Color `YtComboBox` dropdown now stays inside `LeftPanel` (`IsDescendantOf` crosses
|
||||
`PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent`), updates the webcam border colour,
|
||||
and does not exit the properties listing.
|
||||
- **Creator check (needs the creator's hands)** — the last piece of the vv feature: switch the
|
||||
resolution dropdown to "Vertical 1080p60", confirm the Live camera tile lands inside the band and
|
||||
the **Circle** option (TASK 59 turned the Shape button into a Rect | Circle choice) turns the tile
|
||||
|
||||
@@ -2275,6 +2275,36 @@ sizes to its **content**, not to the ComboBox — measured 142.7px of popup for
|
||||
whose rows read "swatch + hex + name". A long row is never clipped, and the popup may be wider
|
||||
than the closed box. Measure with `ActualWidth` in a throwaway test; do not guess widths.
|
||||
|
||||
**2026-10-22 follow-up — `PopupRoot` IS a `Visual`, yet its `VisualTreeHelper.GetParent` is `null`:**
|
||||
Switching from a code-built `ContextMenu` (which is not in `MainWindow`'s routed-event path) to a
|
||||
`ComboBox` (`PART_Popup`) exposed a latent bug in `LeftPanel.IsDescendantOf`:
|
||||
```csharp
|
||||
child = child is Visual or System.Windows.Media.Media3D.Visual3D
|
||||
? VisualTreeHelper.GetParent(child)
|
||||
: LogicalTreeHelper.GetParent(child);
|
||||
```
|
||||
When the user clicked a colour row inside the open `ComboBox` popup, `PreviewMouseLeftButtonDown`
|
||||
tunnelled through `MainWindow.Window_PreviewMouseLeftButtonDown` first. `IsDescendantOf` walked up
|
||||
`VisualTreeHelper.GetParent` to `PopupRoot` (the root `Visual` of the popup's separate Win32
|
||||
`HwndSource`). Because `PopupRoot` IS a `Visual`, the ternary took the `VisualTreeHelper.GetParent`
|
||||
branch and got `null` — never crossing `PopupRoot`'s **logical** parent
|
||||
(`LogicalTreeHelper.GetParent(popupRoot) == PART_Popup`). `LeftPanel.IsClickInside` returned
|
||||
`false`, and `MainWindow` set `SelectedElement = null` (exiting the properties listing before the
|
||||
`ComboBoxItem` click could register). Fix: always coalesce
|
||||
`VisualTreeHelper.GetParent(child) ?? LogicalTreeHelper.GetParent(child) ?? (child as FrameworkElement)?.TemplatedParent`.
|
||||
|
||||
## 2026-10-22 — WINDOWS GIT IDENTITY + PRE-TASK GIT STATUS (RECIPE)
|
||||
|
||||
- **Windows `git` vs WSL `~/.gitconfig`:** the creator's `user.name` / `user.email` lived only in
|
||||
WSL's `~/.gitconfig`, so running `git commit` from the Windows PowerShell host failed with
|
||||
`Author identity unknown`. Fixed permanently in `.git/config` (`[user] name = gramps`,
|
||||
`email = gramps@llamachile.shop`). If you ever need the repo's commit author without running a
|
||||
shell command, read `.git/logs/HEAD` directly with `read_file`.
|
||||
- **Check `git status` BEFORE starting a task** (and never run multiline/compound shell commands
|
||||
when a single simple command or `read_file`/`edit_file` works): `verify.sh` runs `scope-check.sh`
|
||||
over `git diff HEAD`, so any pre-existing uncommitted files in the worktree will fail step 3 at
|
||||
the end of a 2-minute build+test run unless spotted up front.
|
||||
|
||||
## 2026-10-22 — SCANNING A GIT HISTORY FOR SECRETS WITHOUT EVER PRINTING ONE (RECIPE)
|
||||
|
||||
A release gate ("the repo goes public — check the history first"). The rule from
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
| 60 | **The border-colour drop-down names every colour** | ✅ Done — 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" — the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) |
|
||||
| 61 | **The geometry fields share one fixed width** | ✅ Done — 2026-10-21 (the creator's "the Y and H boxes should be the same width as the X and W boxes and those widths should be fixed to accomodate the max number of possible digits" — the Y/H fields used to stretch to fill the row; all four are now 44px, wide enough for 1920 plus a sign) | [`TASKS/task-61-geometry-field-widths.md`](TASKS/task-61-geometry-field-widths.md) |
|
||||
| 62 | **The border-colour field shows the colour's name next to the hex** | ✅ Done — 2026-10-21 (the creator: "the border color drop down box content should also include the name of the color next to the hex value as they in the drop-down list box" — the field keeps the hex editable and the palette's name sits right beside it, empty for a custom hex) | [`TASKS/task-62-border-colour-name-in-the-field.md`](TASKS/task-62-border-colour-name-in-the-field.md) |
|
||||
| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (the creator's "why does the drop down under the web cam properties not show the same style for the border color drop down list box?" + "we established look-and-feel standards. And yet you keep ignoring these" — the hand-rolled `...` button and its code-built menu are gone; the border colour is now a `YtComboBox` like every other picker, the closed box shows the swatch + the colour's name, the rows show swatch + hex + name, and the hex field stays for a custom colour) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) |
|
||||
| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (popup row click deselection fixed 2026-10-22: `LeftPanel.IsDescendantOf` now crosses `PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent` when `VisualTreeHelper.GetParent` returns `null`, so clicking a colour in the open `YtComboBox` updates `BorderColor` instead of exiting properties) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -31,6 +31,17 @@ not *"is this control type themed?"*.
|
||||
matches `SelectedValue` ordinally (case-sensitively) and the palette mixes `#FFFFFF` and
|
||||
`#e94560`; the model matches by VALUE, so the hex the creator typed in any case still lands
|
||||
on the right row.
|
||||
- **Clicking a popup row stays inside `LeftPanel` (2026-10-22 fix):** a `ComboBox`'s open
|
||||
`PART_Popup` lives in its own `PopupRoot` visual tree (`VisualTreeHelper.GetParent(popupRoot)`
|
||||
is `null`), connected back to the `Popup` and `ComboBox` only via `PopupRoot`'s **logical**
|
||||
parent (`LogicalTreeHelper.GetParent(popupRoot)`). `LeftPanel.IsDescendantOf` previously used
|
||||
`child is Visual ? VisualTreeHelper.GetParent(child) : LogicalTreeHelper.GetParent(child)`,
|
||||
which returned `null` at `PopupRoot` because `PopupRoot` is a `Visual`. That caused
|
||||
`MainWindow.Window_PreviewMouseLeftButtonDown` to see `IsClickInside == false` on any click
|
||||
inside the open dropdown and clear `SelectedElement = null` — exiting the properties panel
|
||||
before the `ComboBoxItem` click could select the colour. `IsDescendantOf` now falls back to
|
||||
`LogicalTreeHelper.GetParent` and `TemplatedParent` whenever `VisualTreeHelper.GetParent`
|
||||
returns `null`.
|
||||
- **The closed box shows the swatch + the colour's NAME; the rows show swatch + hex + name**
|
||||
(the rows the creator approved in TASK 60). A ComboBox cannot do that with one
|
||||
`ItemTemplate` — WPF copies it into the read-only `SelectionBoxItemTemplate` — so
|
||||
|
||||
@@ -311,7 +311,13 @@ C# / WPF (.NET 8) following MVVM:
|
||||
the `YtComboBox` popup carries no width binding, so the pop-up is as wide as its widest row
|
||||
(measured 142.7px for the border-colour rows against a 100px ComboBox) — a row is never clipped,
|
||||
and the popup may be wider than the closed box. Measure (`RenderTargetBitmap`/`ActualWidth` in a
|
||||
throwaway test), don't guess.
|
||||
throwaway test), don't guess. **And a `Popup`'s visual root (`PopupRoot`) has a `null` visual
|
||||
parent (`VisualTreeHelper.GetParent(popupRoot) == null`)** even though `PopupRoot` IS a `Visual`:
|
||||
its only link back to the `Popup` and the owning `ComboBox` is `LogicalTreeHelper.GetParent`. Any
|
||||
ancestor walk used by a window-level click-outside guard (`LeftPanel.IsDescendantOf`) MUST fall
|
||||
back to `LogicalTreeHelper.GetParent(child)` when `VisualTreeHelper.GetParent(child)` returns
|
||||
`null`, or clicking a `ComboBoxItem` inside `PART_Popup` looks like an outside click and
|
||||
deselects `SelectedElement` before the item click registers (fixed 2026-10-22).
|
||||
- **A field in a dense panel carries a FIXED width (TASK 61):** a WPF element sizes itself to its
|
||||
content, and in a `DockPanel` the **last child fills the row** — so a row of same-kind fields looks
|
||||
broken unless every field has an explicit `Width` AND the row sets `LastChildFill="False"`. The
|
||||
@@ -2068,13 +2074,13 @@ Apply this to every UI decision:
|
||||
> Everything below is the historical paid-tier design.
|
||||
|
||||
Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar
|
||||
product, discount code, or license key exists any more** — billing is Microsoft Store IAP.
|
||||
product, discount code, or license key exists any more** — nothing bills today; Polar is dormant.
|
||||
Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model.
|
||||
|
||||
Free forever: **all features unlocked for everyone** — no feature lock between free and paid.
|
||||
The **only difference is watermarking**. This is deliberate: no creator will tolerate a watermark,
|
||||
and as a good-guy developer, we give them complete access to every feature so no one can call us
|
||||
crooked.
|
||||
Free forever: **all features unlocked for everyone** — no feature lock, no paid tier, and since
|
||||
2026-10-08 **no watermark either** (the credit is OFF by default; see the banner above). No
|
||||
creator will tolerate a watermark, and as a good-guy developer we give everyone complete access
|
||||
so no one can call us crooked. Money comes from gratuity/merch, not gating (TASK 54).
|
||||
|
||||
- **Free:** a periodic branding credit — "made with LlamaCasty!" in **full-frame neon** (white core,
|
||||
feathered red/blue halo with the channels split in opposite directions), at a **random position
|
||||
@@ -2122,34 +2128,24 @@ crooked.
|
||||
finish. That's it. No feature gating. Alerts, social bar slots, voice filters, TRAX, recording —
|
||||
everything is free. **How `IsPremium` is obtained is a billing concern and has changed twice;
|
||||
what it gates has not.**
|
||||
- **Pricing (✅ DECIDED 2026-09-27 — supersedes the 2026-09-21 one-time "own it" model):**
|
||||
**`$10/mo` subscription, or `$400` lifetime. No annual. No `.99` prices.** ⛔ The old `$29`
|
||||
founder → `$49` list lifetime ruling is **dead — do not cite it.** Two tiers, one delta: they
|
||||
break even at **month 40**, so they barely compete, which is why lifetime is safe to offer at
|
||||
launch instead of saving it for later. The free tier is the *full* app (only the watermark
|
||||
differs), so accessibility is preserved without pricing down. ⛔ No annual tier: Microsoft does
|
||||
not pro-rate and **the developer cannot refund** the forfeited remainder — verbatim the
|
||||
grievance that drove the ruling. **ADA §6(h): raising the price disables auto-renew, so `$10` is
|
||||
effectively locked for the product's life.** Full reasoning in `MONETIZATION.md` → Pricing.
|
||||
- **Billing: ⛔ Polar is DELETED — Microsoft Store IAP, Microsoft as merchant of record** (ruling
|
||||
2026-09-27; teardown in `TASKS/task-48-distribution-msix.md` item 0). **Revenue share 15%,
|
||||
subscriptions included** — verified from the ADA v8.10 PDF, §6(b)(i); the agreement's own v8.0
|
||||
changelog says *"85/15 revenue share for non-Game subscriptions"*. ⇒ net **~$8.50/mo**,
|
||||
**~$340** lifetime. Microsoft handles VAT/GST/sales tax, refunds, chargebacks, the customer
|
||||
portal, and device deactivation — the strongest argument for the switch. Net receipts are after
|
||||
tax, so commission is not paid on pass-through tax. Payouts monthly, $50 threshold.
|
||||
⚠️ **No Polar product, org ID, key prefix, or discount code exists any more** — `d105dfa1-…`,
|
||||
`c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/`LLAMA50`, the customer-portal validate endpoint, and the
|
||||
14-day offline-grace machine are all being deleted. Products are declared in **Partner Center**
|
||||
instead: one auto-renewing monthly subscription + one non-expiring lifetime IAP.
|
||||
⛔ A discounted subscriber→lifetime upgrade SKU is **not** approved.
|
||||
- **Pricing: ⛔ SUPERSEDED 2026-10-08 by the free-forever ruling (TASK 54).** Retained as
|
||||
reasoning only: `$10/mo` subscription or `$400` lifetime, no annual, ruled 2026-09-27 and
|
||||
overtaken ten days later. If a paid tier ever returns, these are the numbers to argue from.
|
||||
**How `IsPremium` would be obtained is a billing concern; what it gates is not.**
|
||||
- **Billing: ⏸ dormant — no paid tier exists** (free-forever ruling 2026-10-08, TASK 54). The
|
||||
Store IAP plan of 2026-09-27 was never implemented (`Windows.Services.Store` appears nowhere
|
||||
in the repo) and was withdrawn with the Store route on 2026-10-22. If a paid tier ever
|
||||
returns, the provider is **Polar** — merchant of record for VAT/sales tax, 5% + $0.50 Starter,
|
||||
**$50 already paid**; `Distribution.md` §4.2/§4.5 holds the design. ⚠️ No Polar product, org
|
||||
ID, key prefix, or discount code exists (`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/
|
||||
`LLAMA50`, the customer-portal validate endpoint, and the 14-day offline-grace machine are all
|
||||
gone).
|
||||
- **Support (creator's model, corrected 2026-09-01):** support = email + GitHub issues — the
|
||||
once-planned in-app bug-reporter is **out of product** (TASKS.md → closed list). Most queries are
|
||||
how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with
|
||||
emergency patches; not a 24/7 service promise. The only license chatter is the paid-user **expiry
|
||||
reminder — now moot** (superseded twice: keys are moot because the lifetime entitlement never
|
||||
expires, and the entire license-key system is moot because Store IAP has no keys); paid users see
|
||||
zero license UI. TASK 36 item 5 (expiry reminder) is struck.
|
||||
emergency patches; not a 24/7 service promise. There is **no licence chatter at all** — the app
|
||||
is free, there are no keys, and no user sees licence UI. TASK 36 item 5 (expiry reminder) is
|
||||
struck.
|
||||
|
||||
**Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1
|
||||
ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by
|
||||
@@ -2569,52 +2565,57 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
|
||||
|
||||
---
|
||||
|
||||
## Windows packaging & distribution (✅ DECIDED 2026-09-27 — Store MSIX + Store IAP)
|
||||
## Windows packaging & distribution (⛔ REVERSED 2026-10-22 — direct + Steam; Store dropped)
|
||||
|
||||
**The distribution route is settled: Microsoft Store, MSIX package, Store IAP.** Creator's
|
||||
criteria, verbatim: *"zero headaches, minimal maintenance (for me) while still providing
|
||||
accountability and a reasonably easy upgrade flow."* Route A was chosen because it is the
|
||||
only option where all four are solved by handing the work to Microsoft rather than to a
|
||||
certificate vendor: **$0/yr, no certificate, no HSM, no annual renewal, no SmartScreen
|
||||
ramp**, plus Store auto-update, Store-side payments/entitlements/refunds/support, and
|
||||
Microsoft review as the accountability layer. The rejected options and their reasons live in
|
||||
`TASKS/research-store-certification.md` §3 — **read that before reopening the question**, not
|
||||
before re-deriving it. Executable checklist: `TASKS/task-48-distribution-msix.md`.
|
||||
**The Microsoft Store route is dead.** A brand name on the Store's *publisher* line requires a
|
||||
**Company** account, which requires a registered legal entity (an LLC — ~$85 New Mexico with a
|
||||
~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an Individual account to
|
||||
a Company account later, and an Individual account publishes under the verified legal name. The
|
||||
creator will not fund an entity for a nameplate, so the 2026-09-27 Store ruling was withdrawn on
|
||||
2026-10-22. Store IAP was never implemented either — `Windows.Services.Store` appears nowhere in
|
||||
the repo.
|
||||
|
||||
**The route now:**
|
||||
|
||||
1. **Direct download from `llamacasty.com`** — the app is **free forever** (TASK 54), so there is
|
||||
nothing to bill in v1.
|
||||
2. **Steam when $100 exists** — Steam Direct is $100 per product (recoupable after $1,000 AGR),
|
||||
accepted categories include *Audio/Video Production* / *Player Tools* (OBS Studio is the
|
||||
precedent), the **developer/publisher name is self-chosen** so the brand needs no entity,
|
||||
5+ screenshots at 1920×1080 + capsule art, a build Valve runs, and a **2-week live page**
|
||||
before release. Valve takes 30%.
|
||||
3. **Polar is dormant, not deleted-in-principle** — chosen checkout/licence provider if a paid
|
||||
tier ever returns; $50 already paid. `Distribution.md` §4.2/§4.5 holds the design.
|
||||
4. **The Microsoft Store: not pursued.** The MSIX package stays as a working artifact and is
|
||||
submitted nowhere. History: `TASKS/research-store-certification.md`,
|
||||
`TASKS/task-48-distribution-msix.md`.
|
||||
|
||||
**Consequences that are architecture, not packaging detail:**
|
||||
|
||||
- **⛔ Velopack and the update URL are deleted** — the Store auto-updates. **DONE 2026-10-22**
|
||||
(TASK 48 slice 3): the `PackageReference` in `ytLive.csproj` and the bootstrap in
|
||||
`App.xaml.cs` are gone, and `ytLive.Tests/StorePackagingTests.cs` guards that no updater
|
||||
package or updater API comes back. The self-hosted DO droplet dies with it.
|
||||
- **The package is built by `scripts/package-msix.sh` (2026-10-22, TASK 48 slice 2)** — publish
|
||||
- **⛔ The updater is now MISSING, not deleted-by-design.** Velopack and its bootstrap were
|
||||
removed on 2026-10-22 (TASK 48 slice 3) because the Store was going to deliver updates — that
|
||||
premise is gone. An update path must come back, and
|
||||
`ytLive.Tests/StorePackagingTests.cs` (which asserts no updater returns) must be rewritten
|
||||
with the chosen updater rather than left to veto one.
|
||||
- **The MSIX package still builds** via `scripts/package-msix.sh` (TASK 48 slice 2) — publish
|
||||
`Release -r win-x64 --self-contained` into `artifacts/msix-stage`, copy
|
||||
`Packaging/Package.appxmanifest` in as `AppxManifest.xml` + `Packaging/Assets/`, then
|
||||
`makeappx pack` → `artifacts/LlamaCasty.msix` (159 MB, gitignored; `MSIX_PFX` signs it for a
|
||||
local install). The manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with
|
||||
`runFullTrust` + webcam + microphone — that is what lets the app launch the bundled ffmpeg as
|
||||
a child process; an `appContainer` package would virtualise the recordings folder instead.
|
||||
`Square310x310Logo`/`Wide310x150Logo` live on `uap:DefaultTile`, **not** on `VisualElements`.
|
||||
Logos come from the app's own artwork via `scripts/make-store-logos.py`.
|
||||
- **⛔ The Polar licensing subsystem is deleted** — `PolarLicenseService`, `PolarLicense`,
|
||||
`MainViewModel.License.cs` (`PremiumUrl`, customer portal, the
|
||||
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy), and the
|
||||
wrong "Polar unlocks alerts" string in `OverlayHost.xaml`. **`IsPremium` is derived from the
|
||||
Store entitlement**, not a remote HTTP call. That deletes the entire "network flaky → app
|
||||
thinks I'm expired" bug class along with the offline-grace machine.
|
||||
- **⛔ Bundle ffmpeg — no runtime downloads** (Store policy 10.2.2; also the 2026-09-01 404).
|
||||
See the FFmpeg locator section above. **This is the first code unit.**
|
||||
`runFullTrust` + webcam + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in
|
||||
`MyMistakes.md`. Logos come from the app's own artwork via `scripts/make-store-logos.py`.
|
||||
- **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a direct
|
||||
download, and it fixes the 2026-09-01 404). See the FFmpeg locator section above.
|
||||
- **⛔ 64-bit Windows only (creator ruling 2026-10-09).** Both csproj files set
|
||||
`<PlatformTarget>x64</PlatformTarget>` (the test project too — an AnyCPU test project
|
||||
referencing an x64 app raises MSB3270 and breaks the 0-warning gate). Why: the bundled ffmpeg
|
||||
is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a
|
||||
32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway. The
|
||||
MSIX publish targets x64 only (ARM64 is a possible later add: both ffmpeg and ONNX Runtime have
|
||||
arm64 builds). Public copy says "64-bit Windows 10 or 11".
|
||||
32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway.
|
||||
Public copy says "64-bit Windows 10 or 11".
|
||||
|
||||
**What does NOT change: the monetization posture.** Free gets everything; the branding flash
|
||||
stays the **only** paid delta (`TASK 36` item 2). Store IAP changes how `IsPremium` is
|
||||
*obtained*, never what it *gates*. `Monetization` above still governs.
|
||||
**What does NOT change: the monetization posture.** The app is **free forever with no watermark**
|
||||
(TASK 54, 2026-10-08), so `IsPremium` gates nothing in v1 and the branding flash is OFF by
|
||||
default. `Monetization` above governs.
|
||||
|
||||
### ⛔ Durable invariant: full trust, or recording silently breaks
|
||||
|
||||
@@ -2642,19 +2643,21 @@ EV-signed file now accrues reputation exactly like an OV-signed one, so EV ($400
|
||||
what OV (~$150–300/yr) buys. Signing is **not** instant regardless — a valid cert stops the
|
||||
*malware* warning immediately, but "unrecognized publisher" clears only as SmartScreen accrues
|
||||
reputation from real download volume. Cert validity is capped at **460 days** (recurring
|
||||
line item) and private keys must live on an **HSM or hardware token**. The **Store MSIX route
|
||||
needs no certificate at all** — Microsoft re-signs. `Distribution.md` §4.3 was corrected
|
||||
2026-09-27; it previously recommended EV.
|
||||
line item) and private keys must live on an **HSM or hardware token**. **The 2026-10-22 ruling
|
||||
is to ship unsigned and accept the "unrecognized publisher" warning** until download reputation
|
||||
accrues — buying a certificate is deferred, not rejected. `Distribution.md` §4.3 was corrected
|
||||
2026-09-27 (it previously recommended EV).
|
||||
|
||||
### Distribution and licensing are independent — but the ruling collapsed them
|
||||
### Distribution and licensing are independent — and the channel decision does not decide licensing
|
||||
|
||||
A **Store-distributed** app *may* still verify licenses with **Polar** (policy 10.8.1
|
||||
explicitly permits a secure third-party purchase API for non-game PC products), so "should we
|
||||
use the Store?" did not logically imply "drop Polar?" — route B was a real option. **The
|
||||
creator chose route A instead**, so Polar is gone. Recording the distinction because it was
|
||||
the reason the decision was thought-through rather than assumed: the licensing choice rode on
|
||||
a distribution decision, and the *only* route that removed the licensing backend was the one
|
||||
that also removed the certificate. Both burdens had the same single solution.
|
||||
A **Store-distributed** app *may* still verify licences with **Polar** (Store policy 10.8.1
|
||||
explicitly permits a secure third-party purchase API for non-game PC products), so "which
|
||||
channel?" never implied "which billing?" — route B was a real option. The 2026-10-22 reversal
|
||||
proves the point in the other direction: dropping the Store did not by itself restart Polar,
|
||||
because the **2026-10-08 free-forever ruling** (TASK 54) had already removed the paid tier.
|
||||
Polar is therefore *dormant*, ready if a paid tier ever returns, and nothing in v1 depends on it.
|
||||
The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a
|
||||
distribution decision.
|
||||
|
||||
### ⛔ "Only when the user says so" is enforced by Windows, not by us
|
||||
|
||||
|
||||
@@ -132,9 +132,107 @@ public sealed class BorderColorPickerTests
|
||||
$"the drop-down rows do not show the colour's name: '{rows}'");
|
||||
|
||||
window.Close();
|
||||
|
||||
// ── 4. clicking a row inside the open popup stays inside LeftPanel ───────
|
||||
// MainWindow_PreviewMouseLeftButtonDown deselects SelectedElement (exiting
|
||||
// the properties listing) whenever LeftPanel.IsClickInside(e.OriginalSource)
|
||||
// returns false. A ComboBox's PART_Popup lives in its own PopupRoot visual
|
||||
// tree whose VisualTreeHelper.GetParent is null — the only link back to the
|
||||
// ComboBox and ElementsScrollViewer is PopupRoot's LOGICAL parent.
|
||||
panel.DataContext = new PanelContext { SelectedElement = element };
|
||||
var hostWindow = new Window { Width = 320, Height = 720, Content = panel };
|
||||
hostWindow.Show();
|
||||
panel.UpdateLayout();
|
||||
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
|
||||
|
||||
var realCombo = FindVisualChildren<ComboBox>(panel)
|
||||
.Single(c => c.ItemsSource is IEnumerable<NamedColor> opts && opts.Any(o => o.Hex == "#e94560"));
|
||||
realCombo.IsDropDownOpen = true;
|
||||
realCombo.UpdateLayout();
|
||||
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
|
||||
|
||||
var crimsonEntry = BorderColorPalette.EntryFor("#e94560");
|
||||
var crimsonItem = realCombo.ItemContainerGenerator.ContainerFromItem(crimsonEntry) as ComboBoxItem;
|
||||
Assert.NotNull(crimsonItem);
|
||||
|
||||
// The click target inside the popup row (e.g. a TextBlock inside the item)
|
||||
// must be recognised as inside LeftPanel, or MainWindow clears SelectedElement.
|
||||
var clickTarget = FindVisualChildren<TextBlock>(crimsonItem!).FirstOrDefault()
|
||||
?? (DependencyObject)crimsonItem!;
|
||||
Assert.True(panel.IsClickInside(clickTarget),
|
||||
"clicking a colour row in the open ComboBox popup is treated as a click outside LeftPanel (exits properties)");
|
||||
|
||||
// Clicking the row in the open popup updates the webcam's BorderColor and closes the popup.
|
||||
crimsonItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
|
||||
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = UIElement.MouseLeftButtonDownEvent,
|
||||
Source = clickTarget,
|
||||
});
|
||||
crimsonItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
|
||||
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = UIElement.MouseLeftButtonUpEvent,
|
||||
Source = clickTarget,
|
||||
});
|
||||
realCombo.UpdateLayout();
|
||||
Assert.Equal("#e94560", element.BorderColor);
|
||||
Assert.Equal(crimsonEntry, realCombo.SelectedItem);
|
||||
Assert.Contains("Crimson", Texts(realCombo));
|
||||
Assert.False(realCombo.IsDropDownOpen);
|
||||
|
||||
// And when starting from a typed custom hex (#123456), picking a palette
|
||||
// row in the open popup still lands cleanly on that palette colour.
|
||||
element.BorderColor = "#123456";
|
||||
realCombo.IsDropDownOpen = true;
|
||||
realCombo.UpdateLayout();
|
||||
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
|
||||
|
||||
var emeraldEntry = BorderColorPalette.EntryFor("#22c55e");
|
||||
var emeraldItem = realCombo.ItemContainerGenerator.ContainerFromItem(emeraldEntry) as ComboBoxItem;
|
||||
Assert.NotNull(emeraldItem);
|
||||
var emeraldTarget = FindVisualChildren<TextBlock>(emeraldItem!).FirstOrDefault()
|
||||
?? (DependencyObject)emeraldItem!;
|
||||
Assert.True(panel.IsClickInside(emeraldTarget));
|
||||
emeraldItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
|
||||
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = UIElement.MouseLeftButtonDownEvent,
|
||||
Source = emeraldTarget,
|
||||
});
|
||||
emeraldItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
|
||||
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
|
||||
{
|
||||
RoutedEvent = UIElement.MouseLeftButtonUpEvent,
|
||||
Source = emeraldTarget,
|
||||
});
|
||||
realCombo.UpdateLayout();
|
||||
Assert.Equal("#22c55e", element.BorderColor);
|
||||
Assert.Equal(emeraldEntry, realCombo.SelectedItem);
|
||||
Assert.Contains("Emerald", Texts(realCombo));
|
||||
|
||||
hostWindow.Close();
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class PanelContext
|
||||
{
|
||||
public SceneElement? SelectedElement { get; set; }
|
||||
public bool IsWebcamSelected => true;
|
||||
public bool CanToggleClipShape => true;
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject root) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
if (child is T match) yield return match;
|
||||
foreach (var nested in FindVisualChildren<T>(child))
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Every text in a visual tree, so a template's content can be read back.</summary>
|
||||
private static string Texts(DependencyObject root)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user