Skip to content

Commit bd10c0b

Browse files
emosaruEmoSaruclaudeEmoSarucodemann8
authored
Port EmoTracker to Avalonia (cross-platform) (#56)
* 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 (&#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> * 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…
1 parent 19d50d6 commit bd10c0b

334 files changed

Lines changed: 21317 additions & 44191 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/launch.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": "0.0.1",
3+
"configurations": [
4+
{
5+
"name": "wiki-annotator",
6+
"runtimeExecutable": "python",
7+
"runtimeArgs": ["-m", "http.server", "8765", "--directory", "D:\\wiki_images"],
8+
"port": 8765
9+
}
10+
]
11+
}

.claude/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(gh repo:*)",
5+
"Bash(gh api:*)"
6+
]
7+
}
8+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
name: github-release
3+
description: Use this skill when the user asks to create a GitHub release, publish a release, cut a release, or make a new release of EmoTracker. Handles version bumping, committing, and triggering the release workflow via a git tag.
4+
---
5+
6+
# GitHub Release
7+
8+
Use this skill to publish a new GitHub release of EmoTracker. The `.github/workflows/release.yml` workflow handles building, packaging, and publishing — this skill bumps the version, commits, and pushes the tag that triggers it.
9+
10+
Follow every step in order — do NOT skip steps or assume defaults.
11+
12+
## Step 1: Interview the user
13+
14+
Before doing anything else, ask the user for the following (use the AskUserQuestion tool if available, otherwise ask directly):
15+
16+
1. **Version number** — The new version in `Major.Minor.Build.Revision` form (e.g. `3.0.2.0`).
17+
2. **Prerelease** — Is this a prerelease? (yes/no)
18+
19+
Do not proceed until you have both answers.
20+
21+
## Step 2: Update assembly versions
22+
23+
Update `AssemblyVersion` and `AssemblyFileVersion` to the new version in ALL of these files:
24+
25+
- `EmoTracker/Properties/AssemblyInfo.cs`
26+
- `EmoTracker.UI/Properties/AssemblyInfo.cs`
27+
- `EmoTracker.Data/Properties/AssemblyInfo.cs`
28+
- `EmoTracker.Core/Properties/AssemblyInfo.cs`
29+
30+
Both attributes in each file should be updated:
31+
```csharp
32+
[assembly: AssemblyVersion("X.Y.Z.W")]
33+
[assembly: AssemblyFileVersion("X.Y.Z.W")]
34+
```
35+
36+
After editing, build to confirm it's clean:
37+
```
38+
dotnet build EmoTracker/EmoTracker.csproj
39+
```
40+
Abort and report the error if the build is not clean (0 errors).
41+
42+
## Step 3: Commit, push, and tag
43+
44+
Commit the version changes with:
45+
```
46+
Bump assembly versions to X.Y.Z.W
47+
```
48+
Include the standard `Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>` trailer. Always use a HEREDOC for the commit message.
49+
50+
Push the commit to the current branch:
51+
```bash
52+
git push origin HEAD
53+
```
54+
55+
Then create and push the release tag. The tag format determines whether the release workflow triggers:
56+
- Stable release: `vX.Y.Z.W`
57+
- Prerelease: `vX.Y.Z.W-preview`
58+
59+
```bash
60+
git tag vX.Y.Z.W # or vX.Y.Z.W-preview
61+
git push origin vX.Y.Z.W # or vX.Y.Z.W-preview
62+
```
63+
64+
## Step 4: Wait for the release workflow
65+
66+
Poll for the workflow run triggered by the tag push. Look for a run on the **Release** workflow (not Build):
67+
68+
```bash
69+
gh run list --limit 10
70+
gh run watch <run-id>
71+
```
72+
73+
If the run fails, report the failure to the user and stop. Do not proceed on a failed run.
74+
75+
## Step 5: Update release notes and mark prerelease
76+
77+
The release workflow creates the GitHub release with placeholder notes. After the workflow completes, replace them with a meaningful summary:
78+
79+
1. Find the previous release tag:
80+
```bash
81+
gh release list --limit 5
82+
```
83+
84+
2. Get the commit log since the previous release (excluding the version bump commit itself):
85+
```bash
86+
git log <previous-tag>..HEAD --oneline
87+
```
88+
89+
3. Write a concise, human-readable summary of the changes. Group related commits together and omit noise (version bumps, CI tweaks) unless they're notable. Use bullet points.
90+
91+
4. Update the release notes:
92+
```bash
93+
gh release edit <tag> --notes "<your summary>"
94+
```
95+
96+
5. If this is a prerelease, also mark it:
97+
```bash
98+
gh release edit <tag> --prerelease
99+
```
100+
101+
Steps 4 and 5 can be combined into a single `gh release edit` call.
102+
103+
## Step 6: Report the release URL
104+
105+
```bash
106+
gh release view vX.Y.Z.W[-preview] --json url --jq .url
107+
```
108+
109+
Report the URL to the user.
110+
111+
## Important notes
112+
113+
- Never skip the interview step.
114+
- Never push the tag before confirming the commit was pushed successfully.
115+
- Never proceed past a failed build (step 2) or a failed workflow run (step 4).
116+
- Match the existing commit style: `Bump assembly versions to 3.0.1.0`.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
name: Build Avalonia (Cross-Platform)
2+
3+
on:
4+
push:
5+
branches: [avalonia]
6+
pull_request:
7+
branches: [avalonia]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
include:
15+
- os: windows-latest
16+
rid: win-x64
17+
artifact: EmoTracker-win-x64
18+
- os: ubuntu-latest
19+
rid: linux-x64
20+
artifact: EmoTracker-linux-x64
21+
22+
runs-on: ${{ matrix.os }}
23+
24+
env:
25+
# Allow restoring the net8.0-windows target on non-Windows (skips WPF-specific build)
26+
EnableWindowsTargeting: true
27+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Setup .NET 8
33+
uses: actions/setup-dotnet@v4
34+
with:
35+
dotnet-version: 8.0.x
36+
37+
- name: Publish
38+
run: dotnet publish EmoTracker/EmoTracker.csproj --framework net8.0 --configuration Release --runtime ${{ matrix.rid }} --self-contained --output publish/raw
39+
40+
- name: Extract version
41+
id: version
42+
shell: bash
43+
run: |
44+
VER=$(sed -n 's/.*AssemblyFileVersion("\([^"]*\)").*/\1/p' EmoTracker/Properties/AssemblyInfo.cs)
45+
echo "app_version=$VER" >> "$GITHUB_OUTPUT"
46+
echo "Detected version: $VER"
47+
48+
- name: Create Linux tar.xz
49+
if: startsWith(matrix.rid, 'linux')
50+
run: |
51+
chmod +x publish/raw/EmoTracker
52+
cd publish/raw
53+
tar cJf ../EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}.tar.xz .
54+
55+
- name: Upload Linux artifact
56+
if: startsWith(matrix.rid, 'linux')
57+
uses: actions/upload-artifact@v4
58+
with:
59+
name: EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}
60+
path: publish/EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}.tar.xz
61+
62+
- name: Bundle PortAudio (Windows only)
63+
if: startsWith(matrix.rid, 'win')
64+
shell: bash
65+
run: |
66+
curl -fsSL "https://github.com/spatialaudio/portaudio-binaries/raw/master/libportaudio64bit.dll" \
67+
-o "publish/raw/portaudio.dll"
68+
69+
- name: Stage Windows output
70+
if: startsWith(matrix.rid, 'win')
71+
run: |
72+
mkdir -p "publish/EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}"
73+
cp -R publish/raw/* "publish/EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}/"
74+
75+
- name: Upload Windows artifact
76+
if: startsWith(matrix.rid, 'win')
77+
uses: actions/upload-artifact@v4
78+
with:
79+
name: EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}
80+
path: publish/EmoTracker-${{ steps.version.outputs.app_version }}-${{ matrix.rid }}
81+
82+
build-macos:
83+
runs-on: macos-latest
84+
85+
env:
86+
EnableWindowsTargeting: true
87+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
88+
89+
steps:
90+
- uses: actions/checkout@v4
91+
92+
- name: Setup .NET 8
93+
uses: actions/setup-dotnet@v4
94+
with:
95+
dotnet-version: 8.0.x
96+
97+
- name: Publish x64
98+
run: dotnet publish EmoTracker/EmoTracker.csproj --framework net8.0 --configuration Release --runtime osx-x64 --self-contained --output publish/x64
99+
100+
- name: Publish arm64
101+
run: dotnet publish EmoTracker/EmoTracker.csproj --framework net8.0 --configuration Release --runtime osx-arm64 --self-contained --output publish/arm64
102+
103+
- name: Extract version
104+
id: version
105+
run: |
106+
VER=$(sed -n 's/.*AssemblyFileVersion("\([^"]*\)").*/\1/p' EmoTracker/Properties/AssemblyInfo.cs)
107+
echo "app_version=$VER" >> "$GITHUB_OUTPUT"
108+
echo "Detected version: $VER"
109+
110+
- name: Create universal binary
111+
run: |
112+
mkdir -p publish/universal
113+
114+
# Copy arm64 as the base (all managed DLLs are identical between architectures)
115+
cp -R publish/arm64/* publish/universal/
116+
117+
# Find all Mach-O binaries and create universal versions with lipo
118+
cd publish
119+
find arm64 -type f | while read arm64_file; do
120+
rel="${arm64_file#arm64/}"
121+
x64_file="x64/$rel"
122+
universal_file="universal/$rel"
123+
124+
if [ ! -f "$x64_file" ]; then
125+
continue
126+
fi
127+
128+
# Check if the file is a Mach-O binary
129+
if file "$arm64_file" | grep -q "Mach-O"; then
130+
# Get architectures of each file
131+
arm64_archs=$(lipo -archs "$arm64_file" 2>/dev/null || true)
132+
x64_archs=$(lipo -archs "$x64_file" 2>/dev/null || true)
133+
134+
if [ "$arm64_archs" = "$x64_archs" ]; then
135+
echo "Skipping $rel (both are $arm64_archs, already identical)"
136+
else
137+
echo "Creating universal binary: $rel ($arm64_archs + $x64_archs)"
138+
lipo -create "$arm64_file" "$x64_file" -output "$universal_file"
139+
fi
140+
fi
141+
done
142+
143+
- name: Create macOS app bundle
144+
run: |
145+
APP="publish/EmoTracker-osx-universal/EmoTracker.app"
146+
mkdir -p "$APP/Contents/MacOS"
147+
mkdir -p "$APP/Contents/Resources"
148+
149+
# Copy universal output into the bundle
150+
cp -R publish/universal/* "$APP/Contents/MacOS/"
151+
cp EmoTracker/macOS/Info.plist "$APP/Contents/"
152+
chmod +x "$APP/Contents/MacOS/EmoTracker"
153+
154+
# Convert .ico to .icns for the app icon
155+
python3 -m venv .venv
156+
source .venv/bin/activate
157+
pip install --quiet Pillow
158+
python3 -c "
159+
from PIL import Image, ImageDraw
160+
import os
161+
162+
ico = Image.open('EmoTracker/emohead_icon_transparent_7h3_icon.ico')
163+
164+
# Extract the largest frame from the ICO
165+
best = None
166+
for size in sorted(ico.info.get('sizes', [(ico.width, ico.height)]), reverse=True):
167+
ico.size = size
168+
candidate = ico.copy().convert('RGBA')
169+
if best is None or candidate.width > best.width:
170+
best = candidate
171+
src = best
172+
173+
iconset = 'EmoTracker.iconset'
174+
os.makedirs(iconset, exist_ok=True)
175+
176+
def make_icon(src, s):
177+
# Create black rounded-rect background, composite icon on top
178+
bg = Image.new('RGBA', (s, s), (0, 0, 0, 0))
179+
draw = ImageDraw.Draw(bg)
180+
r = max(s // 5, 4)
181+
draw.rounded_rectangle([0, 0, s - 1, s - 1], radius=r, fill=(0, 0, 0, 255))
182+
resized = src.resize((s, s), Image.LANCZOS)
183+
bg.paste(resized, (0, 0), resized)
184+
return bg
185+
186+
sizes = [16, 32, 64, 128, 256, 512]
187+
for s in sizes:
188+
icon = make_icon(src, s)
189+
icon.save(os.path.join(iconset, f'icon_{s}x{s}.png'))
190+
if s >= 32:
191+
half = s // 2
192+
icon.save(os.path.join(iconset, f'icon_{half}x{half}@2x.png'))
193+
194+
icon = make_icon(src, 1024)
195+
icon.save(os.path.join(iconset, 'icon_512x512@2x.png'))
196+
"
197+
iconutil -c icns EmoTracker.iconset -o "$APP/Contents/Resources/EmoTracker.icns"
198+
199+
# Ad-hoc code sign so macOS doesn't report the bundle as damaged
200+
codesign --force --deep -s - "$APP"
201+
202+
- name: Create macOS tar.gz
203+
run: |
204+
cd publish/EmoTracker-osx-universal
205+
tar czf ../EmoTracker-${{ steps.version.outputs.app_version }}-osx-universal.tar.gz EmoTracker.app
206+
207+
- name: Upload macOS artifact
208+
uses: actions/upload-artifact@v4
209+
with:
210+
name: EmoTracker-${{ steps.version.outputs.app_version }}-osx-universal
211+
path: publish/EmoTracker-${{ steps.version.outputs.app_version }}-osx-universal.tar.gz

0 commit comments

Comments
 (0)