Fix map location blips clipped at map boundaries - #22
Merged
Conversation
Set ClipToBounds=False on the locations ItemsControl and its Canvas ItemsPanel so blip markers can visually overflow the map image edges, restoring the v2 behaviour where blips near map boundaries were fully visible. Fixes #15 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
emosaru
added a commit
that referenced
this pull request
Apr 17, 2026
* Avalonia migration: Phases 2 & 3 — framework upgrade and WPF decoupling
Phase 2 — Migrate EmoTracker.Core and EmoTracker.Data to net8.0
- EmoTracker.Core.csproj: TargetFramework net472 → net8.0
- EmoTracker.Data.csproj: TargetFramework net472 → net8.0; remove redundant
System.IO.Compression reference (built-in to .NET 8)
- DotNetFrameworkVersion.cs: replace Windows-registry-dependent body with
a no-op stub (net8.0 has no .NET Framework version to check)
NOTE: EmoTracker and EmoTracker.UI still target net472. Phase 4 will add
multi-targeting (net8.0-windows) to those projects to reunify the build.
Phase 3 — Decouple WPF types from extension interface and services
3.1 Extension interface (Extension.cs)
- StatusBarControl: FrameworkElement → object so the interface has no
UI-framework dependency; implementations return the platform control
3.2 Replace Application.Current.Dispatcher with Dispatch.BeginInvoke
- AutoTrackerExtension, MemorySegment, MultiWorldClientSession,
MultiWorldExtension, TwitchExtension, ApplicationModel: all direct
Dispatcher.BeginInvoke calls replaced with Core.Services.Dispatch.BeginInvoke
3.3 Replace DispatcherTimer with System.Timers.Timer
- AutoTrackerExtension.cs: mUpdateTimer converted to System.Timers.Timer
- ApplicationModel.cs: package-refresh timer and notification-expiry timer
both converted to System.Timers.Timer
3.4 Introduce IDialogService and IWindowService abstractions
- New interfaces: Services/IDialogService.cs, Services/IWindowService.cs
- WPF implementations: Services/DialogService.cs (WpfDialogService),
Services/WindowService.cs (WpfWindowService — cross-platform OpenFolder
and OpenUrl using explorer/open/xdg-open)
- ApplicationModel.cs: all MessageBox.Show, Microsoft.Win32 file dialogs,
Keyboard.Focus, Application.Current.MainWindow.Width/Height, and
Process.Start for folder/URL replaced with service calls
- ApplicationModel.cs no longer imports System.Windows.Input or
Microsoft.Win32 dialog types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Avalonia migration: Phase 4 — multi-targeting (net8.0-windows + net8.0)
EmoTracker.UI.csproj
- TargetFrameworks: net472 → net8.0-windows;net8.0
- UseWpf conditioned on net8.0-windows
- Markdig.Wpf conditioned on net8.0-windows
- Avalonia 11.2.7 + Avalonia.Xaml.Behaviors + Markdown.Avalonia 11.0.2
added under net8.0 (source populated by Phase 5)
- All existing WPF source excluded from net8.0 build (placeholder until
Phase 5 adds Avalonia replacements)
EmoTracker.csproj
- TargetFrameworks: net472 → net8.0-windows;net8.0
- UseWpf conditioned on net8.0-windows; OutputType=Library for net8.0
- ApplicationManifest and AutoGenerateBindingRedirects conditioned on Windows
- WINDOWS define constant set for net8.0-windows
- ConnectorLib, PresentationFramework.Aero, NDI project reference all
conditioned on net8.0-windows
- System.Speech and System.ComponentModel.Composition migrated from
bare <Reference> to NuGet PackageReference (8.0.0 / 4.7.0)
- Markdig.Wpf and WpfScreenHelper conditioned on net8.0-windows
- CopyNativeDependencies and GenerateInstaller build targets conditioned
on net8.0-windows
- All WPF-dependent and Windows-only source files excluded from net8.0
via conditioned <Compile Remove> items
Notification.cs
- Removed unused System.Windows and System.Windows.Threading usings so
Notification and MarkdownNotification compile on net8.0
Properties/AssemblyInfo.cs
- Removed top-level System.Windows using
- ThemeInfo assembly attribute wrapped with #if WINDOWS (WPF-only type)
ApplicationModel.cs
- Fixed stray closing paren left from earlier Dispatcher→Dispatch migration
(})); → });) in PushMarkdownNotification
Services/WindowService.cs
- Added missing using System; (needed for OperatingSystem.IsWindows())
Build result: net8.0-windows (WPF) and net8.0 (Avalonia placeholder)
both compile clean with 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Phase 5: Migrate EmoTracker.UI to dual-target (net8.0-windows + net8.0)
All controls, converters, and the image pipeline in EmoTracker.UI now compile
for both the WPF (net8.0-windows) and Avalonia (net8.0) targets using #if WINDOWS
conditional compilation.
Key changes:
- EmoTracker.UI.csproj: Add WINDOWS define, Avalonia/Markdig/SkiaSharp packages,
explicit WPF-only file exclusions (MarkdownViewer.xaml.cs, Settings.Designer.cs)
- IconUtility: Avalonia path uses SkiaSharp for pixel ops (color key, grayscale,
brightness, saturation, overlay compositing); alpha masks cached for hit testing
- ImageReferenceService + all 3 resolvers: return IImage (Avalonia) or ImageSource (WPF)
- InputMaskingImage: Avalonia version uses pointer event filtering + precomputed
alpha mask from IconUtility; WPF version unchanged
- ObservableUserControl, MouseOnlyButton/ToggleButton: namespace swaps
- All 9 converters: System.Windows.Data → Avalonia.Data.Converters; type-specific
changes for ThicknessConverter (Avalonia.Thickness) and InverseTransformConverter
(Matrix.TryInvert)
- MarkdownToFlowDocumentConverter: WPF-only (#if WINDOWS)
- MarkdownProcessor.AsHtml: shared; AsFlowDocument: WPF-only
- MarkdownViewer.cs: new Avalonia code-only implementation using
Markdown.Avalonia.MarkdownScrollViewer + StyledProperty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Phase 6: Migrate main EmoTracker app to Avalonia (net8.0 target compiles clean)
- Add Avalonia 11.2.7 packages and Program.cs entry point for net8.0
- Add App.axaml / App.axaml.cs using OnFrameworkInitializationCompleted
- Add MainWindow.axaml / MainWindow.axaml.cs (Avalonia Window, custom chrome)
- Port all 16 UI AXAML controls: TrackableItemControl, LayoutControl,
LocationControl, LocationMapControl, ChestListControl, CapturableItemControl,
NoteTakingIconPopup, NoteTakingSiteView, MarkdownTextNoteControl,
OverrideExportDialog, PackageManagerWindow, DeveloperConsole,
AppUpdateWindow (stub), GroupedLocationListControl, ItemGridControl,
TwitchStatusIndicator, VariantSwitcherControl
- Update DispatchService / DialogService / WindowService with #if WINDOWS guards
- Guard ApplicationModel ShowDialog calls and ListCollectionView for net8.0
- Add NullToFalseConverter, NonZeroToBoolConverter, BoolInverseConverter,
InverseBoolConverter to EmoTracker.UI.Converters
- Fix LayoutControl.axaml: DataTemplates→UserControl.DataTemplates, GroupBox→Border
- Fix AppUpdateWindow.axaml: remove EmoTracker.Update namespace reference
- Fix CapturableItemControl.axaml: StrokeDashArray comma syntax, PlacementMode
Both net8.0 and net8.0-windows targets now build with 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Phase 7 & 8: Async dialogs (Avalonia) and publish profiles
Phase 7 — ApplicationModel async dialogs:
- Add async method variants to IDialogService (ShowYesNoCancelAsync,
ShowYesNoAsync, ShowOKAsync, OpenFileAsync, SaveFileAsync)
- Add AvaloniaDialogService using MsBox.Avalonia 3.0.0-rc2 for message
boxes and Avalonia StorageProvider for file pickers
- WpfDialogService gains async impls wrapping sync via Task.FromResult
- ApplicationModel: convert 6 command handlers to async void, using
await on dialog calls (InstallPackage, UninstallPackage, RefreshHandler,
ResetUserDataHandler, OpenHandler, SaveAsHandler)
- Add MsBox.Avalonia 3.0.0-rc2 package reference for net8.0 target
Phase 8 — Publish profiles:
- Add Properties/PublishProfiles/ with four publish profiles:
win-x64 (net8.0-windows, self-contained, single-file)
osx-x64, osx-arm64, linux-x64 (net8.0, self-contained, single-file)
- Usage: dotnet publish /p:PublishProfile=<name>
Both net8.0 and net8.0-windows targets build with 0 errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add avalonia-win-x64 publish profile for testing Avalonia build on Windows
Targets net8.0 (Avalonia) with win-x64 RID — same OS as the WPF build
but exercises the cross-platform code path without Windows-only extensions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Avalonia startup crashes: pack:// URIs, missing resources, FontAwesome
- Register pack: URI scheme in Program.cs so PackageManager/LocationDatabase
field initializers (new Uri("pack://application:,,,/...")) don't throw
UriFormatException on .NET 8
- Add avares:// and pack:// translation support to IconUtility.GetImage(Uri)
so embedded resources are loaded via Avalonia AssetLoader
- Add <AvaloniaResource> items to EmoTracker.csproj for net8.0 target so
icons and Resources/** are embedded as Avalonia assets (not WPF <Resource>)
- Define FontAwesome5Free/FontAwesome5Brands FontFamily resources in App.axaml
to fix InvalidCastException from unresolved StaticResource keys
- Fix MainWindow.axaml icon source to use avares:// URI
- Add public parameterless constructor to AppUpdateWindow (AVLN3001)
- Replace deprecated PlacementMode= with Placement= in two Popup declarations
- Suppress NU1701 for WebSocketSharp (no net8.0 target, compat shim is fine)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix Avalonia main window: pack display, drag, title bar clipping
- NoPackagePlaceholder was always visible (no IsVisible binding), covering
the tracker content — add IsVisible binding via NullToTrueConverter on
Tracker.Instance.ActiveGamePackage
- LayoutControl DataContext binding used RelativeSource on MainWindow.ActiveLayout
but the shadowed INotifyPropertyChanged event broke change notifications —
replace with direct TrackerLayout.DataContext assignment in RefreshTrackerLayout()
- Window dragging not implemented — add TitleBar_PointerPressed handler that
calls BeginMoveDrag(e) on left-click of non-Button areas
- ExtendClientAreaToDecorationsHint caused Avalonia to inset content by the OS
title bar height, clipping the custom chrome buttons — remove the two extend
client area attributes; SystemDecorations="BorderOnly" provides the resize border
- Change Window Background from debug magenta (#ff00ff) to #111111
- Add NullToTrueConverter to VisibilityConverters.cs (returns true when null)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Avalonia: fix layout rendering to match WPF visually
LayoutControl.axaml:
- Add ItemContainerTheme to DockPanel template so DockPanel.Dock is
forwarded from each item's DockLocation string via StringToDockConverter
- Add ItemContainerTheme to CanvasPanel template so Canvas.Left/Top/ZIndex
are forwarded from each item's CanvasX/Y/Depth via CanvasPositionConverter
and CanvasZIndexConverter
- Add Width/Height (NegativeToNaN) and IsHitTestVisible bindings to all
DataTemplate Grid wrappers, matching WPF LayoutItemStyle behaviour
- Move GroupBox DataTemplate before Container: Avalonia uses first-match
inheritance lookup, so the more-derived type must be declared first
- Rewrite GroupBox DataTemplate as two-row Grid (header bar + content area)
with HeaderBackground/Background colour bindings, matching WPF LayoutGroupBox
LocationMapControl.axaml:
- Add Background binding to map-location Border using new
AccessibilityLevelToBrushConverter so accessibility colours are shown
- Add IsVisible="{Binding Location.HasVisibleSections}" to location Grid
VisibilityConverters.cs:
- Add StringToDockConverter (string → Dock, case-insensitive)
- Add NegativeToNaNDoubleConverter (−1 → NaN for Width/Height)
- Add CanvasPositionConverter (≤0 → 0.0 for Canvas.Left/Top)
- Add CanvasZIndexConverter (≤0 → 0 for Canvas.ZIndex)
- Add StringToBrushConverter (colour name/hex string → IBrush)
- Add AccessibilityLevelToBrushConverter (AccessibilityLevel → IBrush
from ApplicationColors.Instance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix 5 Avalonia UI issues: map borders, backgrounds, title bar, package manager, right-click
- Add DoubleToThicknessConverter; use it in LocationMapControl for map marker BorderThickness
(fixes invisible outer border on map location squares)
- Add Background binding to ArrayPanel/DockPanel/CanvasPanel/Container/ScrollPanel DataTemplate
Grids in LayoutControl.axaml (fixes wrong/missing backgrounds on panels)
- Add Padding="0" to all 6 title bar chrome buttons in MainWindow.axaml
(fixes icon clipping in the 25px title bar row)
- Add PackageGroup class + AvailablePackagesGroupedView property in ApplicationModel; update
PackageManagerWindow.axaml to bind to it instead of AvailablePackagesView.Groups
(fixes package manager showing no packages in Avalonia build)
- Remove Button.ContextMenu from TrackableItemControl; add Grid_PointerReleased handler that
executes mRegressCmd on right-click directly (fixes empty context menu instead of right-click action)
- Fix ArrayPanel DataTemplate to respect Orientation (vertical/horizontal) via StackPanel bound
to DataContext.Orientation through RelativeSource on ItemsControl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix color key and alpha masking for item images (issue 5b)
SKBitmap.Decode() may return Rgb888x (no alpha channel) for RGB PNG or 24-bit BMP
sources. For that color type, SetPixel(Transparent) is a silent no-op — the alpha
byte stays forced to 255 — so magenta color-key pixels became opaque black instead
of transparent, and the SkToAvalonia alpha mask was all-true (hit testing broken).
Fix: promote the decoded bitmap to Bgra8888 before the color-key loop in GetImage().
Apply the same promotion in ToSkBitmap() so ApplyOverlayImage always composites with
a real alpha channel (otherwise Max(base.Alpha, overlay.Alpha) would be 255 everywhere
and transparent regions in stacked/layered item images couldn't be preserved).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix overlay compositing by caching Skia PNG bytes to bypass Bitmap.Save alpha loss
Avalonia's Bitmap.Save(Stream) may strip the alpha channel on some platforms.
ToSkBitmap used it to convert IImage back to SKBitmap for overlay blending.
When alpha was stripped, every pixel appeared fully opaque (alpha=255), making
the overlay formula treat the entire overlay as a solid mask — the base image
was completely obliterated and only the last layer of stacked items was visible.
Fix: SkToAvalonia now always stores the raw Skia-encoded PNG bytes in sPngCache
alongside the alpha mask. ToSkBitmap reads from that cache first, bypassing
Bitmap.Save entirely. The Bitmap.Save path is kept as a fallback only for images
that did not originate from SkToAvalonia (e.g. GetImageRaw file/avares bitmaps).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix location map UI: popups, pinned locations, chest list, and HTTP images
- LocationMapControl: replace IsLightDismissEnabled with manual popup
management; use DoubleTapped event for pin (not PointerPressed ClickCount);
set LocationControl DataContext imperatively to bypass OverlayLayer
ElementName binding limitation; add badge hover popup
- LayoutControl: move RecentPinnedLocations DataTemplate before ArrayPanel
so the derived type's template wins Avalonia's first-match lookup
- LocationControl: add compact-mode layout (SectionsItemsPanel,
SectionHorizontalAlignment, SectionItemMargin computed properties);
bind ChestListControl.Compact; use AccessibilityLevelToBrushConverter
for section name foreground; prevent width stretch in pinned panel
- ChestListControl: pre-compute per-slot images in ObservableCollection
instead of WPF MultiDataTrigger; add CurrentCompactImage StyledProperty;
cache all display-relevant StyledProperty values in fields so UpdateChests
never calls GetValue() during visual tree teardown; only trigger
UpdateChests for the 7 display-relevant properties to avoid crash when
Avalonia fires inherited property changes on popup overlay close
- IconUtility: fix alpha channel loss (SKAlphaType.Opaque → Premul via
SKCanvas copy); add async HTTP image loading with ConcurrentDictionary
cache and HttpImageLoaded event
- ApplicationModel: subscribe to HttpImageLoaded with debounced refresh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix LocationControl: pin icon, black rectangles, compact title
- Add PushPinCheckBox ControlTheme: FontAwesome pushpin () with
:not(:checked) rotation (90°) and :disabled visibility, replacing the
plain CheckBox that showed Fluent theme's default checkmark
- Fix TrackableItemControl (GateItem/HostedItem) IsVisible bindings: change
from {Binding GateItem/HostedItem, Converter=NullToFalse} to
{Binding Converter=NullToFalse} — the DataContext is already set to the
item on the same element, so the path was resolving against the item
itself (not the parent Section), silently failing and always showing true
- Add TitleText computed property to LocationControl: returns ShortName
when Compact=true, Name otherwise; notifies on CompactProperty and
DataContextProperty changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add DropShadowDirectionEffect support for layout items
Implements the drop shadow feature for layout elements using Avalonia's
DropShadowDirectionEffect (BlurRadius=15, ShadowDepth=0, Opacity=0.8),
matching the centred-glow appearance of the WPF DropShadowEffect.
Adds BoolToDropShadowEffectConverter and wires Effect binding to all
outer Grid containers in LayoutControl.axaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Apply unconditional drop shadow to location info popover
Adds a DropShadowDirectionEffect (BlurRadius=15, ShadowDepth=0,
Opacity=0.8) directly on the LocationControl inside the map's
LocationDetails popup, matching the WPF version's glow appearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix layout element margins not being applied in Avalonia
Avalonia's TypeConverter (ThicknessTypeConverter) is only used during
XAML literal parsing, not at binding resolution time. A {Binding Margin}
where the source is a plain string therefore silently falls back to
Thickness(0). Add StringToThicknessConverter (Avalonia-only) and wire
it into every Margin binding in LayoutControl.axaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing MinWidth/MinHeight/MaxWidth/MaxHeight on layout elements
The WPF LayoutItemStyle conditionally applied all four size-constraint
properties via DataTrigger. The Avalonia version only bound Width and
Height, silently dropping every min/max constraint from layout JSON.
A MaxHeight omission is the direct cause of the pinned-locations panel
growing unboundedly instead of capping at its configured size. Missing
MinWidth/MaxWidth constraints also explain inconsistent resize behaviour.
Adds NegativeToZeroDoubleConverter (MinWidth/MinHeight, -1 → 0) and
NegativeToInfinityDoubleConverter (MaxWidth/MaxHeight, -1 → ∞) to
mirror the WPF DataTrigger guard, and wires all four into every outer
Grid wrapper in LayoutControl.axaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Temporarily disable update check during development
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add VS user files and Claude local settings to .gitignore
Ignores *.user, *.suo, .vs/, and .claude/settings.local.json which
are developer-machine-specific and should not be tracked.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix layout sizing: LayoutTransformControl, SizeToContent, Bounds fallback
MainWindow.axaml:
- Replace RenderTransform on TrackerScaleGrid with LayoutTransformControl.
RenderTransform is post-layout only so Ctrl+scroll zoom left dark background
visible (scale < 1) or clipped content (scale > 1). LayoutTransformControl
participates in the layout pass like WPF's LayoutTransform.
MainWindow.axaml.cs:
- Add UpdateResizeMode() to mirror WPF's AllowResize=false behaviour:
sets SizeToContent=WidthAndHeight and CanResize=false so the window shrinks
to the pack's natural content size (same as WPF SizeToContent trigger).
- Subscribe to Tracker.PropertyChanged so AllowResize changes at runtime update
the window's resize mode (e.g. when switching packs).
- Fix RefreshTrackerLayout() to fall back to Width/Height when Bounds is 0×0.
At construction time Bounds has not been measured yet; using 0>0=false always
picked horizontal layout regardless of the configured window size.
LayoutControl.axaml:
- Add HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
to the root ContentControl so the DataTemplate-generated layout always fills
the full content area rather than relying on Avalonia's Left/Top defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add per-element LayoutTransform scale to layout DataTemplates
Implements the LayoutItem.Scale / OverrideScale feature in Avalonia,
equivalent to WPF's LayoutItemStyle DataTrigger that applied a
LayoutTransform when OverrideScale=true.
- Add LayoutItem.EffectiveScale (returns Scale when OverrideScale, else 1.0)
- Wrap every LayoutControl DataTemplate's content in LayoutTransformControl
bound to EffectiveScale so the per-element scale participates in the
layout pass (Margin/HAlign/VAlign moved to LTC; Width/Height/Min/Max
remain on the inner Grid, matching WPF ContentPresenter semantics)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix layout fill: add HorizontalContentAlignment=Stretch to item containers
In WPF, ContentPresenter.HorizontalContentAlignment defaults to Stretch,
so DataTemplate content fills its item container and layout constraints
flow correctly (DockPanel LastChildFill, Viewbox scaling, etc.).
In Avalonia it defaults to Left, causing each layout element to be
arranged at its desired/natural size — the map panel dictated layout
size instead of filling the remaining window space.
Fix: add a shared StretchItemContainer ControlTheme and apply it as
ItemContainerTheme on every layout-hosting ItemsControl (ArrayPanel,
DockPanel, CanvasPanel, ViewBox, GroupBox, Container, ScrollPanel).
DockPanel and CanvasPanel already had inline themes for attached
properties; Stretch setters are merged into those.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Container/GroupBox panels: use Grid instead of default StackPanel
The default ItemsControl panel is a vertical StackPanel, which measures
children with infinite height. This breaks DockPanel.LastChildFill
downstream — the map gets infinite remaining space and measures at its
natural image size rather than filling the window's available space.
Container (JSON types "container"/"grid") maps to a single-cell Grid
in WPF. Switching the ItemsPanel to Grid passes finite height
constraints through the layout chain, so the DockPanel receives the
actual window content height and the map's Viewbox scales to fit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix RecentPinnedLocations: use WrapPanel with orientation binding
The ItemsControl used the default vertical StackPanel, ignoring the
pack JSON orientation/style settings. Pinned location cards stacked
vertically instead of flowing horizontally with wrapping.
Fix: use WrapPanel with Orientation bound to the model's Orientation
property (parsed from the pack's "orientation"/"style" fields).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix item margins, ButtonPopup styling, and location PreserveDimension
- Add StringToThicknessConverter to ItemGridControl margin bindings
(Avalonia doesn't auto-convert string→Thickness in bindings)
- Apply NegativeToNaNDoubleConverter to Item DataTemplate IconWidth/IconHeight
- Implement full ButtonPopup DataTemplate with gear icon, image, and popup
- Add HeaderContent support to GroupBox DataTemplate header bar
- Add PreserveDimension binding to RecentPinnedLocations LocationControls
- Add ComputedMaxWidth/MaxHeight constraints to LocationControl
- Move PreserveDimension enum to EmoTracker.UI for cross-project access
- Add ObjectEqualsConverter and OrientationToPreserveDimensionConverter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix map orientation switching with imperative code-behind
Bindings from inside ItemsPanelTemplate to ancestor controls don't
reliably resolve in Avalonia. Walk the visual tree to find the named
MapsPanel StackPanel and set its Orientation imperatively on load,
aspect ratio change, and DataContext change.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix pinned locations column alignment by removing explicit Left alignment
WPF defaults HorizontalAlignment to Stretch, allowing LocationControls
to fill the WrapPanel column width uniformly. The explicit Left alignment
in the Avalonia template prevented this, causing uneven column widths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix ButtonPopup sizing: use icon pixel dimensions for unspecified sizes
Move Popup outside LayoutTransformControl (matching WPF structure) so popup
content is not scaled by the button's EffectiveScale. Add IconDimensionMultiConverter
that falls back to the source bitmap's pixel dimensions when IconWidth/IconHeight
are unspecified (-1/NaN), instead of using NaN auto-sizing which interacts badly
with parent layout transforms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix F2 and F11 keyboard shortcuts, add map location visibility logic
- F2: Implement BroadcastView window for Avalonia and wire up
ShowBroadcastView command (was a no-op with #if WINDOWS guard)
- F11: Add MapLocationVisibilityConverter that replicates WPF's
MultiDataTrigger logic for hiding cleared/empty locations based on
DisplayAllLocations, shift key, ForceVisible/ForceInvisible
- Use tunnel routing for KeyDown to match WPF's PreviewKeyDown behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use native window chrome, move app buttons to status bar
Replace custom titlebar (BorderOnly + manual drag/min/max/close) with
native system decorations. Move Settings, Package Manager, and Refresh
buttons to the left side of the status bar for cross-platform compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix pinned locations panel: respect Style property for panel selection
The RecentPinnedLocations template was hardcoded to WrapPanel, ignoring
the ArrayPanel.Style property. The default Style is Stack, which in WPF
uses a StackPanel that stretches items to full width. Added
PanelStyleToTemplateConverter to select StackPanel or WrapPanel based on
the Style and Orientation properties via MultiBinding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix pushpin icon rotation using LayoutTransformControl wrapper
The WPF version used LayoutTransform on the Grid to rotate the pin icon
90° when unpinned. Avalonia Grid doesn't support LayoutTransform, so wrap
it in a LayoutTransformControl to properly adjust the layout box during
rotation, preventing clipping and position offset.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add settings context menu and package manager icon color indicators
Implement the settings gear button context menu matching the WPF build:
Application, Layout, Tracking, Extensions, Assistance, and Advanced
submenus with all checkable options and command bindings.
Add dynamic foreground color to the package manager icon based on
update availability: cyan when updates exist or no packages installed,
yellow when the active package has an update, gray default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Package Manager UI: font sizes, sorting, search delay, and styling
Port WPF's RepoEntryGameNameSort logic to Avalonia's LINQ grouping so
packages sort by series priority, game priority, and official/featured
flags. Resolve game display names via FindGame for correct group headers.
Add 500ms debounced search (replacing WPF Binding Delay=500), rounded
search bar with clear button, set base FontSize to 12 to match WPF
defaults, center action button text and sidebar icons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Package Manager column alignment with SharedSizeGroup
Add Grid.IsSharedSizeScope and SharedSizeGroup attributes to align
Name, Author, Status, and Action columns across all package entries.
Disable horizontal scrolling so the star-width progress bar column
respects viewport bounds, override ProgressBar's theme MinWidth to
prevent overlap, and restore SizeToContent=Width for auto-expanding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add Installed Packages menu, native checkboxes, and resize grip
Build the Installed Packages submenu dynamically with grouped/sorted
packages, author display, variant sub-items, and Active badge. Replace
CheckBox-in-MenuItem.Icon with native ToggleType="CheckBox" so clicking
anywhere on the menu item toggles the setting. Add a resize grip visual
in the status bar bottom-right corner, visible when resizing is enabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add auto-tracking service provider abstraction and SNI provider
Decouple auto-tracking from Windows-only ConnectorLib by introducing a
provider/device abstraction layer (IAutoTrackingProvider, IAutoTrackingDevice)
in EmoTracker.Data. Providers are discovered via reflection using
[AutoTrackingProvider] attribute. Packs can optionally specify preferred
providers via manifest "auto_tracker_providers" field.
- ConnectorLibProvider wraps existing ConnectorLib code (Windows-only)
- SniProvider adds cross-platform SNES auto-tracking via gRPC to SNI
- AutoTrackerExtension and internal types no longer reference ConnectorLib
- Avalonia AutoTrackerExtensionView for cross-platform UI
- All gRPC awaits use ConfigureAwait(false) to prevent UI thread deadlocks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove ConnectorLib, improve SNI provider and UI
- Remove ConnectorLib provider, assembly references, native dependencies,
and External/ConnectorLib directory entirely
- Refactor AutoTrackingProviderRegistry to use TypedObjectRegistry
- Add options, operations, and device status to context menus (WPF + Avalonia)
- Fix async deadlock with ConfigureAwait(false) on all gRPC awaits
- Fix context menu rebuild timing (rebuild on open, not on property change)
- SNI: default address space to SnesABus, auto-detect memory mapping via
MappingDetect on connect with fallback re-detection on Unknown mapping error
- SNI: set User-Agent header to "EmoTracker/<version>" on gRPC channel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Avalonia capture item popup and WPF context menu
Fix capture item popup in Avalonia: items were being manipulated instead
of captured because RelativeSource AncestorType bindings can't cross
Popup visual tree boundaries. Set ClickHandler in code-behind and use
named element bindings instead. Also fix UnknownControl dashed border
visibility to only show when no item is captured.
Fix WPF AutoTracker context menu crash by moving it out of Style.Setter
(which doesn't support event handlers) to a direct Grid property.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix Avalonia TabPanel rendering with proper templates
Add ItemTemplate and ContentTemplate to the TabControl in the Avalonia
TabPanel DataTemplate. Tab headers now render icon and title instead of
ToString(). Tab content uses ContentControl to render the LayoutItem
via inherited DataTemplates, since LayoutControl expects a Layout object
with a Root property rather than a bare LayoutItem.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix nullable annotation warnings and member hiding warning
Add #nullable enable annotations to 14 files that use nullable syntax
without a nullable context, resolving all CS8632 warnings. Using
annotations-only mode avoids triggering nullable flow analysis warnings
in code not yet fully annotated.
Fix CS0108 in GroupedLocationListControl by adding 'new' keyword to
PropertyChanged event that hides AvaloniaObject.PropertyChanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix remaining build warnings
Replace obsolete PlacementMode property with Placement in
LocationMapControl.axaml (AVLN5001). Suppress SYSLIB0014 WebClient
warnings in PackageManager and PackageRepository — WebClient is deeply
integrated with async event patterns and not worth refactoring now.
Net8.0 build is now 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add GitHub Actions CI for Avalonia cross-platform builds
Build and publish self-contained net8.0 binaries for Windows (x64),
macOS (x64 + arm64), and Linux (x64) on pushes and PRs to the
avalonia branch. Artifacts are uploaded for download.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix CI restore command and simplify to restore+publish
Remove invalid -f flag from dotnet restore (not supported, caused
MSB1008 on Linux). Use long-form flags for clarity. Combine build
and publish into a single publish step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix CI: enable cross-platform restore for multi-target project
Set EnableWindowsTargeting=true so the net8.0-windows target can be
restored on non-Windows runners without errors. Remove separate restore
step (publish handles it). Add fail-fast: false so all platforms build
independently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove VariantSwitcher extension
Delete all VariantSwitcher source files (WPF and Avalonia) and clean up
csproj exclusion rules. The extension auto-discovered via reflection,
so no registration code needed cleanup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add macOS .app bundle packaging to CI
Create proper macOS application bundles (.app) for osx-x64 and
osx-arm64 builds with Info.plist, correct directory structure
(Contents/MacOS/), and executable permissions. This allows macOS
to recognize and launch the application.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix macOS app bundle: add code signing, icon, and tar.gz packaging
- Ad-hoc codesign the .app bundle to prevent "damaged" Gatekeeper error
- Convert Windows .ico to macOS .icns using Pillow and iconutil
- Package macOS builds as .tar.gz to preserve Unix executable permissions
(zip artifacts lose the +x bit, causing launch failures)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix macOS CI: use venv for Pillow install
macOS runner uses externally-managed Python (PEP 668) that blocks
system-wide pip install. Use a virtual environment instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix macOS icon alpha channel transparency
Extract the largest frame from the ICO and convert to RGBA before
resizing. Without explicit RGBA conversion, Pillow composites against
a grey background, losing transparency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Package Linux build as tar.xz to preserve permissions
Ensure the EmoTracker executable has +x and package as tar.xz to
preserve Unix permission bits. Windows remains as a plain zip artifact.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use black rounded-rect background for macOS app icon
Composite the icon onto a black rounded rectangle instead of relying
on transparency, which macOS fills with a grey background. Icon is
inset with a small margin for a clean look.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add native macOS menu bar with app name and settings menus
Sets Application Name="EmoTracker" so the menu bar displays correctly,
and adds a NativeMenu with File, View, Tracking, Packages, Advanced,
and Help menus replicating the gear/settings context menu items
rearranged per macOS conventions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Style Avalonia tab panels to match WPF appearance
Custom TabItem template with pill-shaped headers (CornerRadius 7),
semi-transparent dark background for unselected tabs, solid background
with border for selected tabs, and hover effects. Horizontal tab flow
via WrapPanel and transparent content area matching WPF behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix macOS menu bar, app icon, keyboard shortcuts, and icon colors
- Move NativeMenu to XAML declaration for reliable macOS menu bar display
- Remove inset margin from macOS app icon so background fills edge-to-edge
- Use Cmd+ instead of Ctrl+ for InputGesture so shortcuts show correctly
on macOS; add HasCmdModifier helper to accept both Control and Meta
- Change note taking and auto tracker inactive icon color to #717171
to match settings, package manager, and reload icons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix dev console, async override icons, remove native menu, suppress console
- Fix developer console not opening on non-Windows (was behind #if WINDOWS)
- Load Export Overrides file icons asynchronously for fast window opening
- Match override search box styling to package manager
- Remove macOS native menu bar (not useful in current state)
- Suppress console window on Windows by using WinExe output type
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Bump all assembly versions to 3.0.0.0 for EmoTracker 3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Include assembly version in CI build artifact filenames
Extracts the version from the published assembly and uses it in
archive filenames and artifact names (e.g. EmoTracker-3.0.0.0-win-x64).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix CI version extraction for macOS (grep -P unsupported)
Replace platform-conditional version extraction with a single sed
command that works on all platforms including macOS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Disable note-taking extension UI and location note sites
Note-taking code is preserved but inactive — extension returns null
for StatusBarControl, and note icons on locations/map pins are
commented out in both WPF and Avalonia views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove application update functionality
Delete AppUpdate singleton, AppUpdateWindow (WPF + Avalonia), and all
references — menu items, startup update checks, and BETA title format.
Package manager "Requires App Update" version gating is preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix tooltip delay binding for items in Avalonia build
Bind ToolTip.ShowDelay to ApplicationSettings.FastToolTips via a
converter matching WPF behavior (5000ms slow, 400ms fast).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Build universal macOS app bundle instead of separate x64/arm64
Publish both osx-x64 and osx-arm64, then merge Mach-O binaries with
lipo to produce a single universal .app that runs natively on both
Intel and Apple Silicon Macs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix universal macOS build: skip lipo for same-architecture binaries
Some NuGet native libraries ship identical architectures for both
RIDs, causing lipo to fail. Skip merging when both files already
have the same architecture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix black rectangles in empty item grid cells
Hide TrackableItemControl with Opacity=0 when DataContext is null
to prevent badge TextBlock background from rendering while preserving
layout space. Also add NonEmptyStringToBoolConverter for badge
visibility and harden null converters against Avalonia UnsetValue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add NWA autotracking provider for Bizhawk emulator
Implement a new autotracking service provider using the NWA (Network
Access) protocol to support multi-platform tracking in Bizhawk. The
provider scans a configurable port range (NWA_PORT_RANGE env var,
default 0xBEEF) for running emulator instances, exposing each as a
separate device.
Key features:
- TCP-based NWA protocol client with full binary/ASCII message handling
- Supports all GamePlatform values (NES, SNES, N64, GB, GBA, GC, Genesis)
- SNES: auto-switches to BSNESv115+ core with save state preservation
- Requires "System Bus" memory domain; fails cleanly if unavailable
- Address mapping framework (INwaAddressMap) for future per-platform maps
- Device names include game title (truncated to 30 chars with ellipsis)
Also updates VS Code launch/tasks config to support both WPF (net8.0-windows)
and Avalonia (net8.0) debug configurations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Improve NWA provider: settings, address maps, and change detection
- Add generic provider_settings key-value store to ApplicationSettings,
persisted in application_settings.json
- SNI provider reads gRPC address from "sni_grpc_address" setting
- NWA provider reads host from "nwa_host" setting
- Use "SRAM" memory domain for SNES SRAM bank mappings
- Add Genesis/Megadrive address map using "M68K BUS" domain
- Add "gen" as a recognized platform string for Genesis
- Remove Reset/Pause/Resume operations from NWA devices
- Detect core/game changes on read/write failure and reinitialize
address map with a single retry
- Fix change detection: record core info before building address map
so stored values reflect the state the map was built against
- Update mPlatform from CORE_CURRENT_INFO so fallback map selection
uses the correct platform after a core change
- Reuse NWA device instances across refreshes so DefaultDevice stays
consistent with AvailableDevices
- Auto-stop autotracking on device disconnect
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Gitignore all Claude Code .local config files
Broadens the ignore pattern from a single file to .claude/*.local.*
and removes the previously tracked settings.local.json.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix missing scrollbars in ScrollPanel layout element
ScrollPanel derives from Container, and Avalonia's DataTemplate matching
picks the first template whose DataType is assignable from the item.
The Container template was declared before ScrollPanel, so it shadowed
ScrollPanel entirely — items rendered with a plain ItemsControl and no
ScrollViewer at all, silently dropping scroll behavior.
Move the ScrollPanel template ahead of the Container template and add
a comment documenting the ordering requirement for future Container
subclasses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Enable NDI broadcast for the Avalonia build
Adds cross-platform NDI support using NDILibDotNetCoreBase so the broadcast
view works on Windows, macOS, and Linux. Key design points:
- Runtime is located via NDI_RUNTIME_DIR_V6 / V5 env vars (set by NDI Tools)
rather than bundled DLLs; V4/V3 are rejected because the wrapper depends
on struct layouts and v3 APIs that are only present from NDI 5+ (and 4.5
cannot be distinguished from the broken 4.0/4.1 via the env var).
- Capture goes through Compositor.CreateCompositionVisualSnapshot instead
of RenderTargetBitmap.Render. ImmediateRenderer walks the visual tree
but never pushes IEffect, so DropShadowDirectionEffect (applied to layout
panels via BoolToDropShadowEffectConverter) was absent from the NDI
output. The snapshot runs the full composition render pipeline so
effects are included. The returned Bitmap is immutable and its
CopyPixels throws, so the snapshot is drawn onto a reusable
RenderTargetBitmap for pixel extraction.
- Premultiplied-to-straight alpha conversion is applied before shadow
compositing and NDI send — Avalonia produces premultiplied BGRA, but
NDI FourCC_type_BGRA and FastDropShadow both expect straight alpha.
Without this, text showed colour fringing and shadow blending was wrong.
- Capture dimensions use TopLevel.RenderScaling so text is captured at
the display's physical pixel density rather than at 96 DPI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Broadcast NDI from a background hidden window on Windows
The NDI source is now advertised and frames are sent whether or not the
user has opened the visible broadcast view. A hidden off-screen Window
owned by NDIExtension hosts an NdiSendContainer; the visible BroadcastView
stays openable at any time but yields NDI responsibility to the hidden
window on Windows.
- ApplicationSettings.EnableBackgroundNdi (default true, persisted as
enable_background_ndi) lets users opt out. Windows-only; the setting
is ignored on macOS/Linux and the visible BroadcastView continues to
own NDI there until a cross-platform hidden-window path is verified.
- NDIExtension manages the hidden window lifecycle via ReconcileHiddenWindow,
triggered on Start, on ApplicationModel.BroadcastLayout changes, and on
ApplicationSettings.EnableBackgroundNdi changes. The window is created
iff Windows && EnableBackgroundNdi && BroadcastLayout has content.
- NdiSendContainer.NdiEnabled (StyledProperty<bool>) lets a container be
attached to the visual tree without initialising NDI. The visible
BroadcastView sets this to false on Windows when background NDI is on,
avoiding a duplicate-source conflict.
- NdiSendContainer.UseExternalCaptureDriver (StyledProperty<bool>) switches
the internal DispatcherTimer from a per-frame capture driver to a slow
250ms receiver-count poller. In this mode an external driver is expected
to call TriggerCaptureAsync (now public) whenever a new frame should be
captured. The slow poll still catches OBS-connect transitions when the
external driver is idle.
- HiddenBroadcastWindow is off-screen (-32000, -32000), ShowInTaskbar=false,
ShowActivated=false, SystemDecorations=None, Opacity=0.01. Opacity must
be >0: Avalonia's compositor skips rasterising fully transparent windows,
which would leave CreateCompositionVisualSnapshot returning blank pixels.
Captures are driven from the main window's RequestAnimationFrame so they
align with real state changes.
- NdiSendContainer.TriggerCaptureAsync runs a synchronous layout pass
(Measure/Arrange) before capturing. An off-screen window's layout
system may not run on its own, leaving Bounds at 0x0.
- Fix an NDI library refcount bug: the visible BroadcastView's dormant
NdiSendContainer used to call NDIlib.destroy() on dispose even though
it had never called NDIlib.initialize(), tearing down the shared
library state while the hidden window was still using it. Gated on
a new _ndiInitialized flag.
- SendThreadProc gets a top-level try/catch so a single bad frame no
longer kills the thread silently.
- Serilog diagnostics [NDI] at all key decision points: reconcile,
window create/close, sender create, render loop start, capture/send
heartbeats, thread start/exit, sender-missing warnings, send failures.
Heartbeats are throttled to one line per 5s per channel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Extend background NDI broadcast to macOS and Linux
Removes the Windows-only gate on HiddenBroadcastWindow so background NDI
now runs on every platform when ApplicationSettings.EnableBackgroundNdi
is true. The off-screen + Opacity=0.01 + main-window RAF capture driver
strategy is platform-independent from Avalonia's point of view, though
each platform's window manager behaves slightly differently for
off-screen absolute positioning:
* Windows: tested, reliable
* Linux X11: most WMs honour -32000/-32000 positioning
* Linux Wayland: client absolute positioning is forbidden by protocol,
so the hidden window will be placed visibly; Opacity=0.01 keeps it
imperceptible but users may see a near-transparent ghost window
* macOS: Cocoa generally honours off-screen positions
These platform caveats are documented in HiddenBroadcastWindow's class
XML doc. The Wayland limitation is noted as a known issue; future work
could use WindowState.Minimized or layer-shell protocols as improvements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix NDI rendering on macOS: HiDPI crop and RGBA pixel format
On macOS with Retina displays, two bugs caused the NDI output to appear
zoomed in/cut off with incorrect colours:
- DrawImage was passing physical pixel dimensions as logical coordinates,
causing the snapshot to render at 2x logical size and clip to the
top-left quadrant on HiDPI displays. Fixed by dividing width/height
by renderScale to convert to logical units.
- Avalonia's Metal backend produces pixels in RGBA order, not BGRA.
The NDI FourCC is now selected per-platform: BGRA on Windows (D3D/Skia)
and RGBA on macOS/Linux (Metal).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Derive NDI FourCC from snapshot pixel format instead of OS check
Rather than inferring the pixel channel order from the OS platform,
read it directly from the Bitmap.Format of the compositor snapshot.
The backend decides the layout (BGRA on D3D, RGBA on Metal) and
Format reflects this, so the FourCC now tracks any future backend
changes automatically without manual platform guards.
Adds FourCC to PendingFrame so the send thread uses the value
recorded at capture time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Use switch to map snapshot PixelFormat to NDI FourCC
Replaces the ternary with an explicit switch over each supported
PixelFormat value, with a default case that logs a warning and falls
back to BGRA if an unrecognised format is encountered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Integrate NetSparkleUpdater for cross-platform auto-update via GitHub Releases
Adds a background update checker and manual "Check for Updates…" menu item
backed by a custom IAppCastDataDownloader that queries the GitHub Releases API
at runtime (no hosted appcast XML required). Downloads are zip archives; a
SparkleUpdater subclass drives the in-place swap-and-relaunch scripts generated
at update time for Windows (.bat) and macOS/Linux (.sh).
Upgrade Avalonia 11.2.7 → 11.3.3 (required by NetSparkleUpdater.UI.Avalonia 3.0.4).
New files:
EmoTracker/Services/Updates/GitHubAppCastDataDownloader.cs
EmoTracker/Services/Updates/ZipInstallAndRelaunch.cs
EmoTracker/Services/Updates/UpdateService.cs
.github/workflows/release.yml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix update detection: pre-release tags, version suffix, and OS filtering
Three bugs prevented update detection from working on macOS:
1. /releases/latest excludes pre-releases (404 until a stable release exists);
switch to /releases?per_page=1 which returns the most recent regardless.
2. Tag names like "3.0.1.1-preview" are unparseable by System.Version;
strip the pre-release suffix before writing sparkle:version into the appcast.
3. A single <item> with multiple enclosures (one per OS) caused NetSparkle to
read the first enclosure's sparkle:os as the item's OS, misclassifying the
item and filtering it out on non-matching platforms. Fix: emit one enclosure
per item matching only the current platform's asset, with the correct
sparkle:os value (omitting sparkle:os defaults to "windows" in NetSparkle).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Use inline HTML release notes via GitHub Markdown API
Replace sparkle:releaseNotesLink (which required a JavaScript-capable
WebView to render GitHub's page) with a <description> CDATA block
containing HTML rendered by the GitHub /markdown API. Falls back to
a <pre>-wrapped plain-text block if the API call fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add custom update dialogs with Markdig-rendered release notes
- Add EmoTrackerUIFactory, EmoCheckingForUpdatesWindow, EmoUpdateAvailableWindow:
custom NetSparkle dialog windows styled to match EmoTracker's dark theme
- Use Markdig to convert GitHub release notes markdown → HTML locally,
displayed via Avalonia.HtmlRenderer HtmlLabel with themed CSS stylesheet
- Fix update detection: strip pre-release suffix from tag versions, emit
per-platform enclosure with sparkle:os, use /releases?per_page=1 endpoint
- Restore AssemblyVersion to 3.0.1.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Re-enable note-taking UI with Markdig/HtmlRenderer rendering
- Re-enable note-taking icon popup on location controls and note indicator
on map pins; re-enable status bar note-taking extension
- Replace Markdown.Avalonia (incompatible with Avalonia 11.3.3) with
Markdig + Avalonia.HtmlRenderer in EmoTracker.UI: MarkdownViewer now
converts markdown → HTML via Markdig and renders with HtmlLabel
- Fix MarkdownTextNoteControl: manage IsVisible in code-behind to work
around ObservableUserControl shadowing Avalonia's INotifyPropertyChanged
event; sync MarkdownSourceEmpty from DataContext via PropertyChanged
subscription so edit button shows/hides correctly
- Style note editing UI to match dark theme: custom TextBox resource
overrides (TextControlBackground/Foreground/Border*), dark edit and
delete buttons, Transparent hit-test padding on icon buttons
- Fix TextBox content clipping by using native Fluent TextBox with
resource overrides instead of a custom ControlTemplate; set row
definitions to Auto so popup sizes to content
- Set IsLightDismissEnabled=False on notes popup and defer TextBox focus
to DispatcherPriority.Loaded to prevent popup closing on edit click
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Polish note-taking and Twitch status indicator UI
- Remove note-taking icon from the status bar (bottom bar)
- Fix note-taking icon vertical alignment in location header (-2px top margin)
- Remove padding from note icon border wrapper
- Match note icon default (no notes) color to pin icon (WhiteSmoke)
- Match Twitch disconnected icon color to autotracker icon (#717171)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove Bonta Multiworld extension
- Delete Extensions/BontaMultiworld directory entirely
- Remove EnableBontaMultiWorld, IgnoreBontaMultiWorldRomCheck, MultiworldNotificationLevel from ApplicationSettings
- Remove Multi-World submenu from Extensions menu in MainWindow.axaml
- Remove now-unnecessary BontaMultiworld exclusion from EmoTracker.csproj
- Clean up stale keys from application_settings.json files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add cross-platform voice control via Vosk + PortAudio
Replace Windows-only System.Speech recognition with Vosk (offline
speech recognition) and PortAudioSharp (cross-platform audio I/O),
enabling the voice control extension on all platforms.
- VoiceRecognitionExtensionAvalonia.cs: full Vosk-based reimplementation
- Grammar-mode recognition: builds flat phrase list from item/location
databases, maps recognized text back to actions
- PortAudioSharp for audio capture and device enumeration
- Platform-native TTS feedback (macOS: `say`, Windows: PowerShell)
- Device preference persisted to application settings
- Model loaded from ~/Documents/EmoTracker/vosk-model/ or next to exe
- VoiceRecognitionStatusIndicator.axaml/.cs: new Avalonia status indicator
- Mic icon (on/off/listening states)
- Context menu: Active toggle + Input Device submenu (live device list)
- ApplicationSettings: add VoiceInputDeviceName (save/load)
- EmoTracker.csproj: include VoiceRecognition in net8.0 build; add Vosk
and PortAudioSharp packages
Note: requires a Vosk English model in vosk-model/ and PortAudio native
library (macOS: brew install portaudio).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Vosk model auto-download with progress UI
When voice control is activated and the model is not present, show a
prompt offering to download vosk-model-small-en-us-0.15 (~40 MB).
If accepted, a progress window matching the update-check dialogue
style shows live download progress, then extracts and installs the
model. On failure the window returns to the prompt with a Retry button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add voice recognition word-split heuristic and phonetic_substitutes item property
- Model.FindWord() recursive heuristic splits unknown compound words into
known sub-words at command map build time (e.g. hookshot → hook shot)
- New ItemBase.PhoneticSubstitutes (string[]) property parsed from
phonetic_substitutes JSON array; all alternates register the same voice command
- BuildCommandMap now iterates GetItemNameVariants() so phonetic substitutes
and auto-split names are both registered for every item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Restrict Twitch chat commands to !tracker prefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add Claude Code settings and github-release skill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove WPF build: drop net8.0-windows target and all WPF-specific sources
- EmoTracker.csproj and EmoTracker.UI.csproj now target net8.0 only;
removed UseWpf, WPF packages (Markdig.Wpf, WpfScreenHelper),
NDILibDotNet2 project reference, and all conditional ItemGroups/targets
- Deleted 45 WPF-specific files: *.xaml/*.xaml.cs pairs, DiscordRPC.cs,
Settings.Designer.cs, app.manifest, VoiceRecognitionExtension.cs
- Stripped #if WINDOWS blocks from 29 shared .cs files, keeping only
the Avalonia branches (ApplicationModel, DialogService, WindowService,
DispatchService, TwitchExtension, all EmoTracker.UI converters/controls)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add up-to-date dialog, rename update UI classes, remove NDILibDotNet2
- Add ApplicationIsUpToDateWindow styled to match UpdateAvailableWindow;
override ShowVersionIsUpToDate() in UpdateUIFactory to show it
- Rename update UI classes: EmoCheckingForUpdatesWindow →
CheckingForUpdatesWindow, EmoUpdateAvailableWindow →
UpdateAvailableWindow, EmoTrackerUIFactory → UpdateUIFactory
- Remove WPF launch/build config from .vscode/launch.json and tasks.json
- Remove NDILibDotNet2 project and External/NDI folder from solution
and delete the directory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add custom update download progress dialog
Implements IDownloadProgress with UpdateDownloadWindow, styled to match
VoskModelDownloadWindow (dark theme, teal progress bar, BorderOnly chrome).
Wired up via UpdateUIFactory.CreateProgressWindow().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix inaccessible section chests not graying out in Avalonia build
Replicates the WPF MultiDataTrigger logic that set Accessible=False and
IsEnabled=False on ChestListControl when AccessibilityLevel==None and
neither the per-section AlwaysAllowChestManipulation nor the global
AlwaysAllowClearing override is enabled.
Adds ChestAccessibleConverter (IMultiValueConverter) and wires it to
the Accessible and IsEnabled properties of ChestListControl via
MultiBinding in LocationControl.axaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Refactor VisibilityConverters.cs: split into focused files by category
Move non-visibility converters out of VisibilityConverters.cs into:
- NumericConverters.cs: Negative*, Canvas*, IconDimensionMultiConverter
- BrushConverters.cs: StringToBrush, AccessibilityLevelToBrush,
BoolToDropShadowEffect, PackageManagerForeground
- LayoutConverters.cs: StringToDock, OrientationToPreserveDimension,
PanelStyleToTemplate
- ThicknessConverter.cs: DoubleToThicknessConverter (existing file)
- StringConverters.cs: PackageManagerTooltipConverter (existing file)
VisibilityConverters.cs now contains only bool/visibility converters
plus the location-specific MapLocationVisibility and ChestAccessible
multi-converters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Move location-specific converters out of VisibilityConverters.cs
MapLocationVisibilityConverter and ChestAccessibleConverter are specific
to location/section UI logic and don't belong with generic visibility
helpers. Moved them to a new LocationConverters.cs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Bump assembly versions to 3.0.1.6
* Bump assembly versions to 3.0.1.7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix hidden NDI broadcast window keeping process alive after main window close
Set ShutdownMode=OnMainWindowClose so Avalonia triggers shutdown (and fires
the Exit event) when the main window closes, rather than waiting for all
windows. This ensures NDIExtension.Stop() is called via OnApplicationClosing,
which destroys the HiddenBroadcastWindow before the process exits.
Also explicitly close BroadcastView in MainWindow.OnClosing and expose it
via a public property on ApplicationModel for that purpose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix infinite NDI log spam for null/unsupported snapshot pixel format (#20)
* Fix infinite NDI log spam for null/unsupported snapshot pixel format
When the compositor returns a snapshot with a null pixel format (visual tree
not yet ready), skip the frame silently instead of logging a warning on every
capture tick. For genuinely unsupported non-null formats, throttle the warning
to once per 5 seconds using the same pattern as LogSenderMissingWarning.
Fixes #11
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix infinite NDI log spam for null/unsupported snapshot pixel format
- Null format now falls back to platform default (RGBA on macOS, BGRA elsewhere)
instead of skipping the frame, so captures continue while the visual tree warms up
- Unsupported format warning logs once per application run (was: once per 5s)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: EmoSaru <emosaru@emosaru.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix broadcast view forcing always-on-top over main tracker window (#21)
Show BroadcastView without an owner window. Passing mainWindow as the
owner to Show() causes the OS to enforce that the owned window always
stays above its owner in z-order, making the broadcast view impossible
to hide behind the tracker. Showing it as an independent top-level
window restores the expected behaviour.
Fixes #18
Co-authored-by: EmoSaru <emosaru@emosaru.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix map location blips clipped at map boundaries (#22)
Set ClipToBounds=False on the locations ItemsControl and its Canvas
ItemsPanel so blip markers can visually overflow the map image edges,
restoring the v2 behaviour where blips near map boundaries were fully
visible.
Fixes #15
Co-authored-by: EmoSaru <emosaru@emosaru.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing backgrounds in RecentPinnedLocations and GroupBox (#23)
* Fix missing backgrounds in RecentPinnedLocations and GroupBox
- RecentPinnedLocations template was missing a Background binding on its
Grid, unlike all sibling container templates (ArrayPanel, Container, etc.).
Added the same StringToBrushConverter binding.
- GroupBox content-area background used FallbackValue=#66212121, which only
fires on binding errors. When StringToBrushConverter returns null (empty
string or null Background value), Avalonia sets the property to null
(transparent). Changed to TargetNullValue so the default dark fill is
applied whenever the converter produces null.
Fixes #16
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix missing backgrounds in RecentPinnedLocations and GroupBox
- RecentPinnedLocations Grid was missing a Background binding; added
StringToBrushConverter binding matching all other container templates
- GroupBox content area used FallbackValue for null Background; changed
to TargetNullValue so the default #66212121 fill applies when the
converter returns null (not just on binding errors)
- ScrollPanel content now has MinHeight bound to the ScrollViewer's
viewport height so DockPanel LastChildFill works correctly: Avalonia's
ScrollViewer passes infinite height to content, collapsing the GroupBox
to zero height when empty; the MinHeight constraint gives the DockPanel
a finite lower bound while still allowing overflow scrolling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix ScrollPanel content not filling viewport for DockPanel LastChildFill
The previous fix (MinHeight only) was insufficient: the default StackPanel
items panel gives each child only its desired height regardless of MinHeight,
so the inner DockPanel never received the viewport height as a finite
constraint and LastChildFill could not allocate remaining space to the
Pinned Locations GroupBox.
Fix: switch…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ItemsControlrendering map location blips uses aCanvassized exactly to the map image, and both theItemsControlandCanvaswere clipping overflow by default in AvaloniaClipToBounds="False"to both theItemsControland itsCanvasItemsPanelso blip markers near the map edge are fully visible, restoring v2 behaviourTest plan
Closes #15
🤖 Generated with Claude Code