Skip to content

Port EmoTracker to Avalonia (cross-platform)#56

Merged
emosaru merged 183 commits intomainfrom
avalonia
Apr 17, 2026
Merged

Port EmoTracker to Avalonia (cross-platform)#56
emosaru merged 183 commits intomainfrom
avalonia

Conversation

@emosaru
Copy link
Copy Markdown
Contributor

@emosaru emosaru commented Apr 17, 2026

Summary

This PR merges the avalonia branch — a full port of EmoTracker from WPF to Avalonia, enabling cross-platform builds (Windows, macOS, Linux) — along with the many stability, autotracking, and UX fixes that have landed on the branch since it was cut.

Highlights:

  • Framework port: WPF → Avalonia 11 across the main app, UI library, and all .axaml/code-behind.
  • Cross-platform: macOS and Linux packaging, updater, and bundle handling (App Translocation detection, Unix file modes, split swap scripts).
  • NDI removal: External/NDI/NDILibDotNet2 and distributables removed as part of the port.
  • Layout/input fixes: MapPanel clip, GroupBox clip, tabbed layout ordering, ViewBox overlay scaling, popup light-dismiss, note popup outside-click dismiss, MaskInput/IgnoreUserInput handling, collection-modified-during-enumeration fixes.
  • Autotracking: SNI periodic scan + reconnect loop, missing-ActiveConnector fix, Lua error reporting improvements.
  • Infrastructure: tag-triggered release workflow, CI Node 24 opt-in, PortAudio bundling, notification system, MCP tooling, Windows Defender CFA handling, LocalApplicationData staging dir.

183 commits; full log available on the branch.

Test plan

  • CI build passes on Windows
  • CI build passes on macOS
  • CI build passes on Linux
  • Manual smoke: launch, load a pack, interact with maps and items
  • Manual smoke: autotracking connects (SNI)
  • Manual smoke: tabbed/docked layouts render correctly
  • Manual smoke: updater flow on Windows and macOS

🤖 Generated with Claude Code

EmoSaru and others added 30 commits April 4, 2026 15:48
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>
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>
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>
…les 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 — 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>
…ndows

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>
…esome

- 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>
- 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>
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>
…e 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>
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>
…ve 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>
…mages

- 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>
- Add PushPinCheckBox ControlTheme: FontAwesome pushpin (&#xf08d;) 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>
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>
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>
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>
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
…back

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>
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>
…iners

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>
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>
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>
- 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>
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>
…ment

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>
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>
- 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>
EmoSaru and others added 28 commits April 15, 2026 10:55
AppDomain.CurrentDomain.SetupInformation.ApplicationBase returns null in
.NET Core/.NET 8, causing SetDllDirectory to receive a relative path that
the loader cannot resolve. AppContext.BaseDirectory is the correct
replacement and always returns the absolute path of the app's base directory.

This fixes portaudio.dll (and any other x64/ bundled natives) failing to
load on the published Windows build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bdir

The .NET runtime's native library probing automatically resolves DLLs
from runtimes/{rid}/native/ without needing SetDllDirectory. Revert the
AppContext.BaseDirectory change and instead place portaudio.dll in the
correct standard location in both CI workflows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove all #if WINDOWS conditional compilation blocks from App.axaml.cs:
  ConfigurePlatformDllPaths/SetDllDirectory call, Discord RPC teardown,
  and the SetDllDirectory P/Invoke declaration
- Drop unused System.Runtime.InteropServices using directive
- Place bundled portaudio.dll alongside the executable in CI output
  rather than in a runtimes subdirectory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Drive locks

Moves the .update-staging directory from inside the app install folder to
%LOCALAPPDATA%\EmoTracker\.update-staging (on Windows) so that:
- Windows Defender Controlled Folder Access cannot block the extraction step
  when the app is installed in Desktop or Documents (both are CFA-protected)
- OneDrive / Known Folder Move cannot lock files mid-copy when those folders
  are being synced

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebClient download handlers fire on ThreadPool threads. Mutating
ObservableCollection (Clear/Add) off the UI thread causes
CollectionChanged notifications to fire on the wrong thread, which
races with UI-thread enumerations and produces the
"Collection was modified; enumeration operation may not execute"
InvalidOperationException.

Parse JSON on the background thread as before, collect results into
a local List<T>, then dispatch a single BeginInvoke to the UI thread
to Clear/Add into mPackages and fire ForceRefreshProperty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a pre-flight check before applying updates on Windows. If Controlled
Folder Access is enabled and the install directory is inside a default
protected folder (Desktop, Documents, Pictures, Videos, Music), the xcopy
swap script would be silently blocked. Now we detect this condition,
show an actionable warning dialog styled to match the existing update UI,
and abort cleanly instead of leaving a partially-applied update.

- WindowsCfaChecker: reads CFA state from registry and checks whether the
  install path is under a default protected folder; no-ops on non-Windows
- CfaWarningWindow: dark-themed dialog explaining the problem and listing
  resolution steps (move the app or add a Windows Security exception)
- ZipInstallAndRelaunch: runs the check as the first step before any
  filesystem work

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…x paths

The macOS release archive packages a full EmoTracker.app bundle at its
root, but the old LaunchUnixSwapScript treated installDir (Contents/MacOS)
as the copy target — nesting the new bundle inside the old one and leaving
the running binary unchanged.

LaunchMacOSSwapScript now:
- Derives the .app bundle path by walking up two levels from installDir
- Removes the old bundle and copies the new one from staging in its place
- Clears the com.apple.quarantine xattr so Gatekeeper doesn't block launch
- Relaunches via `open` (required for proper bundle context on macOS)

LaunchLinuxSwapScript retains the existing flat-file copy logic.
Both Unix paths now use UseShellExecute = false for predictable behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dismiss, MCP tool (#53)

- Render notifications with a drop shadow matching layout element shadow config
- Apply type-specific left-border accent colors (Message=green, Celebration=cornflower blue, Warning=amber, Error=red)
- Expose notification colors in ApplicationColors with JSON-configurable keys (notification_message/celebration/warning/error)
- Add NotificationTypeToBrushConverter that reads live from ApplicationColors.Instance
- Fix click-to-dismiss: replace Opacity=0 button overlay (Avalonia hit-test dead) with full-card Button; add ForceExpired event for immediate removal bypassing the timer
- Add push_notification MCP tool for all four NotificationTypes

Co-authored-by: EmoSaru <emosaru@emosaru.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…of style override (#54)

Style setters (Style Selector="TabControl") have lower property-value priority
than local values and are evaluated after the Fluent theme ControlTheme has
already been applied, creating a window where the theme template can render
tabs in its own order. Setting TabControl.Template directly as a local value
ensures the custom template is in effect before the visual tree is attached,
guaranteeing tabs render in their JSON definition order.

Also inlines ItemsPresenter.ItemsPanel directly in the template rather than
via a separate Style Selector="TabControl ItemsPresenter" setter, removing
another style-timing dependency.

Co-authored-by: EmoSaru <emosaru@emosaru.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore the VariantSwitcher status bar extension removed in a5dbe9c,
following the same enable/disable pattern as the voice control
extension. Disabled by default via a new EnableVariantSwitcher setting
(also exposed through the MCP settings tool). The folder icon uses the
#717171 default-state color to match other extension icons, and the
context menu is populated programmatically on click so both left and
right mouse buttons open the current pack's variant list reliably.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enable IsLightDismissEnabled on the notes Popup so clicking outside
dismisses it, and sync the toggle button's IsChecked state back to
false on Popup.Closed so the next click reopens the popup instead of
silently toggling off.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the fixed sleep 1 with a kill -0 poll loop so the swap never
races a still-exiting EmoTracker instance (mirroring the Windows tasklist
loop). Launch the script via nohup + & so it survives the parent App
bundle exiting and cannot be killed by SIGHUP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…FileMode

GetAwaiter().GetResult() on RunProcessAsync deadlocks when called from
the Avalonia UI thread because the async continuation tries to resume on
the same thread that is blocked waiting for it. Replace the chmod process
spawn with File.SetUnixFileMode (available since .NET 7) which is
synchronous and needs no child process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redirect script stdout/stderr to /tmp/emotracker_update.log with set -x
so every command and its result is visible after the update runs. Also
log computed paths (installDir, appBundle, stagingDir) via Serilog before
the script launches. Fixes CA1416 warnings by switching RuntimeInformation
checks to OperatingSystem.IsX() guards the analyzer understands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Needed to produce a CI build of an older version than 3.0.1.15 so the
update flow can be exercised end-to-end against a real release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a quarantined app is launched from ~/Downloads, macOS Gatekeeper
runs it from a randomized read-only mount under /AppTranslocation/.
AppContext.BaseDirectory returns that translocated path, so the swap
script's rm/cp target the read-only clone and fail silently — the real
bundle is never updated and 'open' just relaunches the old version.

Detect the translocation path prefix and show a dedicated warning
window instructing the user to move the app out of Downloads, matching
the existing Windows CFA pre-flight pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Update-flow testing is complete; bump back to 3.0.1.15 so the next
release is forward from the previous public build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LayoutTransformControl for MapPanel defaults to Stretch alignment
and inherits ClipToBounds=False from the LayoutControl-scoped style,
so the scaled map could extend its hit-test region past its slot and
swallow clicks on neighbouring layout elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The release workflow now builds and publishes on tag push, so the skill
bumps the version, pushes, and tags instead of manually downloading
artifacts and creating the release with gh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@emosaru emosaru requested a review from a team April 17, 2026 18:12
@emosaru emosaru merged commit bd10c0b into main Apr 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants