Skip to content

Fix update staging dir to avoid CFA blocks and OneDrive locks - #52

Closed
emosaru wants to merge 160 commits into
mainfrom
avalonia
Closed

Fix update staging dir to avoid CFA blocks and OneDrive locks#52
emosaru wants to merge 160 commits into
mainfrom
avalonia

Conversation

@emosaru

@emosaru emosaru commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Moves .update-staging from inside the app install directory to %LOCALAPPDATA%\EmoTracker\.update-staging (Windows) / LocalApplicationData/EmoTracker/.update-staging (macOS/Linux)
  • Prevents Windows Defender Controlled Folder Access from blocking the extraction step when EmoTracker is installed in Desktop or Documents (both are CFA-protected folders by default)
  • Prevents OneDrive / Known Folder Move sync from locking files mid-copy when those directories are being cloud-synced

Test plan

  • Trigger an update with the app installed in %USERPROFILE%\Desktop\EmoTracker\ and confirm it applies cleanly
  • Trigger an update with the app installed in %USERPROFILE%\Documents\EmoTracker\ and confirm it applies cleanly
  • Verify .update-staging no longer appears inside the install directory after an update
  • Verify %LOCALAPPDATA%\EmoTracker\.update-staging is created and cleaned up during the update
  • Confirm update still works from a non-special directory (e.g. D:\Games\EmoTracker\)

🤖 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 13, 2026 00:34
…nd building

Pack load for Codetracker Map Tracker was taking ~7.8s. Three optimizations:

1. Dictionary index for GetPersistableLocationReference/ItemReference:
   Replace O(n) List.IndexOf with O(1) dictionary lookup. LocationDatabase
   and ItemDatabase now maintain a Dictionary<T, int> populated on insert.
   Saves ~2.9s from 583 location lookups during voice command building.

2. Defer BuildCommandMap to background thread: Voice recognition command
   map building (~5s) now runs on Task.Run with snapshots of item/location
   data. Atomic swap under lock ensures thread safety. Cancellable on
   pack switch. Completely removes voice recognition from the critical path.

3. Code-to-provider index in ItemDatabase: New BuildCodeIndex() creates a
   Dictionary<string, List<ITrackableItem>> mapping codes to their providers.
   LuaItems (dynamic Lua callbacks) fall back to brute-force iteration.
   All item types implement GetAllProvidedCodes() to expose their static
   code sets. ProviderCountForCode and related methods use the index when
   available, avoiding iteration over all items for each code query.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… exception handling

The developer console (ScriptManager) is intended for pack developers to
debug their Lua scripts and pack definitions. Infrastructure subsystem
messages from Voice Recognition, NDI, and MCP were leaking into it via
the Serilog DeveloperConsoleSink, confusing users with errors unrelated
to their packs (e.g. PortAudio enumeration failures, NDI init warnings).

Changes:
- DeveloperConsoleSink now filters out log messages prefixed with
  [Voice], [NDI], or [MCP]. These still go to the file log and stdout.
- NdiSendContainer.OnAttachedToVisualTree wrapped in try-catch so NDI
  init/runtime errors are caught and logged rather than propagating.
- NdiSendContainer.Dispose NDI cleanup wrapped in try-catch to prevent
  exceptions during teardown from surfacing.

Closes #37

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Optimize pack load performance (~7.8s to <1s)
…t, and popup light-dismiss

TrackableItemControl now uses a standard Image (rectangular hit test) by default
and only uses InputMaskingImage (per-pixel alpha) when MaskInput is true.
IgnoreUserInput is enforced via IsHitTestVisible binding, CanExecute checks,
and an early guard in Grid_PointerReleased.

LocationMapControl popup dismiss now uses a tunneling PointerPressed handler on
the top-level window so clicks on controls outside the map (e.g. tracker items)
properly close the location details popup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix input handling and map popup light-dismiss
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tunneling PointerPressed handler that closes the location details popup
on outside clicks was also closing it when clicking inside child popups
(e.g. the capture grid). Popup content renders on an overlay layer with a
disconnected visual tree, so the walk from e.Source never reached
LocationDetailsContent for nested popups.

Fix: detect clicks inside popup overlays by checking whether the visual root
of e.Source is the TopLevel window. If not, the click is inside a popup
overlay and should not dismiss the location details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix capture grid dismissed by map popup light-dismiss
- Remove case-insensitive ToLower() calls in ItemDatabase code provider
  indexing, preserving Lua's case-sensitive code lookups
- Allow IClickHandler to intercept clicks on IgnoreUserInput items by
  making CanExecute always return true (guard moves to Execute, after
  IClickHandler check)
- Remove IsHitTestVisible binding that blocked all interaction on
  IgnoreUserInput items
- Add xpcall/debug.traceback wrapper (SafeCall) for all Lua call sites
  to capture full Lua call stacks in error output
- Add dedicated LuaException formatting in developer console
- Pass script path to DoString for better error source identification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix doors tracking and improve Lua error reporting
Child content that exceeds a GroupBox's bounds was visually overflowing
instead of being clipped. Add ClipToBounds="True" to the outer Grid in
the GroupBox DataTemplate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ClipToBounds to GroupBox layout element
Iterate temporary .ToList() snapshots of Repositories and Packages
collections in UpdatesAvailable and CurrentPackageHasUpdateAvailable
to avoid InvalidOperationException when collections are modified
concurrently. Add null checks on entries for safety.

Based on codemann8's fix (codemann8/EmoTracker@617da7a).

Fixes #14

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix collection modified during enumeration in package update checks
- Add ActiveConnector property to AutoTrackerExtension as an alias for
  ActiveProvider, restoring compatibility with Lua scripts that rely on
  the old property name
- Exclude [SNI] and [NWA] log messages from the developer console, as
  they are internal infrastructure concerns not relevant to pack developers

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

- SniProvider now owns its ConnectionStatusChanged event, decoupled from
  the current DefaultDevice instance so subscriptions survive device
  changes during reconnect
- RefreshDevicesAsync preserves existing SniDevice instances across
  refreshes (matched by URI) so event wiring stays stable
- A scan timer starts on first RefreshDevicesAsync call (when the user
  selects the provider) and refreshes the device list every 5 seconds,
  removing stale entries and auto-selecting the first device when one
  becomes available
- When autotracking is active, the loop also attempts reconnection every
  5 seconds if the device is present but not connected
- Added AvailableDevicesChanged event to IAutoTrackingProvider so the
  view layer can react to background device list updates
- AutoTrackerExtension subscribes to AvailableDevicesChanged on
  SelectedProvider to auto-select a device and invalidate Start command
  availability without requiring manual provider re-selection
- AutoTrackerExtensionView pulses the status icon grey<->teal (~4s
  cycle) when a valid provider and device are ready but autotracking has
  not yet been started

Closes #46

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ViewBox DataTemplate: add Grid ItemsPanel so items use 4-value margin
as absolute edge offsets instead of stacking vertically with the default
StackPanel. Add Background binding (needed for semi-transparent overlays
like start_location_view). Add ClipToBounds=false on ItemsControl.

GroupBox DataTemplate: change content panel from StackPanel to Grid so
children receive a finite height constraint. StackPanel measured children
at infinite height, causing Viewbox-based content (map overlays) to scale
based on width only—filling the width and clipping vertically on resize.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e PortAudio in Windows CI

- Add AudioLibrariesAvailable property to VoiceRecognitionExtension; set
  false if Vosk or PortAudio native libraries fail to load
- Status indicator shows red mic-slash icon and descriptive tooltip when
  libraries are unavailable; Active/Input Device context menu items are
  disabled in that state
- CI workflows (build-avalonia and release) download the pre-built
  PortAudio x64 DLL from spatialaudio/portaudio-binaries and place it in
  the x64/ subdirectory of the Windows publish output (Windows only)

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
@emosaru
emosaru requested a review from a team April 16, 2026 06:47
@emosaru emosaru closed this Apr 16, 2026
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