diff --git a/WheelWizard/Services/ModManager.cs b/WheelWizard/Services/ModManager.cs index 3cce2a06..b51cb587 100644 --- a/WheelWizard/Services/ModManager.cs +++ b/WheelWizard/Services/ModManager.cs @@ -30,7 +30,6 @@ private set } } - private bool _isProcessing; private bool _isBatchUpdating; private ModManager() @@ -107,17 +106,23 @@ private void Mod_PropertyChanged(object sender, PropertyChangedEventArgs e) if (_isBatchUpdating) return; - if ( - e.PropertyName != nameof(Mod.IsEnabled) - && e.PropertyName != nameof(Mod.Title) - && e.PropertyName != nameof(Mod.Author) - && e.PropertyName != nameof(Mod.ModID) - && e.PropertyName != nameof(Mod.Priority) - ) + if (e.PropertyName == nameof(Mod.Priority)) + { + SaveModsAsync(); + SortModsByPriority(); return; + } - SaveModsAsync(); - SortModsByPriority(); + if ( + e.PropertyName == nameof(Mod.IsEnabled) + || e.PropertyName == nameof(Mod.Title) + || e.PropertyName == nameof(Mod.Author) + || e.PropertyName == nameof(Mod.ModID) + ) + { + SaveModsAsync(); + OnPropertyChanged(nameof(Mods)); + } } private void SortModsByPriority() @@ -207,12 +212,18 @@ await Task.Run(() => public void ToggleAllMods(bool enable) { - foreach (var mod in Mods) + _isBatchUpdating = true; + try + { + foreach (var mod in Mods) + mod.IsEnabled = enable; + } + finally { - mod.IsEnabled = enable; + _isBatchUpdating = false; } - _isProcessing = !_isProcessing; + SaveModsAsync(); OnPropertyChanged(nameof(Mods)); } diff --git a/WheelWizard/Views/App.axaml b/WheelWizard/Views/App.axaml index f7afb145..1b9991a0 100644 --- a/WheelWizard/Views/App.axaml +++ b/WheelWizard/Views/App.axaml @@ -49,27 +49,27 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - + + + + + + + diff --git a/WheelWizard/Views/App.axaml.cs b/WheelWizard/Views/App.axaml.cs index e1d16884..32ba582c 100644 --- a/WheelWizard/Views/App.axaml.cs +++ b/WheelWizard/Views/App.axaml.cs @@ -6,6 +6,7 @@ using WheelWizard.Services; using WheelWizard.Services.LiveData; using WheelWizard.Services.UrlProtocol; +using WheelWizard.Views.Behaviors; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement; using WheelWizard.WiiManagement.GameLicense; @@ -38,6 +39,13 @@ public void SetServiceProvider(IServiceProvider serviceProvider) public override void Initialize() { AvaloniaXamlLoader.Load(this); + InitializeBehaviorOverrides(); + } + + private void InitializeBehaviorOverrides() + { + //Behavior overrides are native components where we are overriding the behavior of + ToolTipBubbleBehavior.Initialize(); } private static void OpenGameBananaModWindow() diff --git a/WheelWizard/Views/Behaviors/ToolTipBubbleBehavior.cs b/WheelWizard/Views/Behaviors/ToolTipBubbleBehavior.cs new file mode 100644 index 00000000..80d17f1c --- /dev/null +++ b/WheelWizard/Views/Behaviors/ToolTipBubbleBehavior.cs @@ -0,0 +1,355 @@ +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace WheelWizard.Views.Behaviors; + +public static class ToolTipBubbleBehavior +{ + private const string BubblePointerLeftClass = "BubblePointerLeft"; + private const string BubblePointerMiddleClass = "BubblePointerMiddle"; + private const string BubblePointerRightClass = "BubblePointerRight"; + private const string BubbleAnimateInClass = "BubbleAnimateIn"; + private const string BubbleAnimateOutClass = "BubbleAnimateOut"; + private const double TailCenterOffsetFromSide = 22d; + private const double TooltipVerticalOffset = -4d; + private static readonly TimeSpan HoverOpenDelay = TimeSpan.FromMilliseconds(200); + private static readonly TimeSpan MinimumVisibleTime = TimeSpan.FromSeconds(1); + private static readonly TimeSpan CloseAnimationDuration = TimeSpan.FromMilliseconds(40); + private static readonly ConditionalWeakTable ToolTipStates = new(); + private static bool _isInitialized; + + public static void Initialize() + { + if (_isInitialized) + return; + + _isInitialized = true; + ToolTip.TipProperty.Changed.AddClassHandler(OnTipChanged); + ToolTip.ToolTipOpeningEvent.AddClassHandler(OnToolTipOpening); + ToolTip.IsOpenProperty.Changed.AddClassHandler(OnIsOpenChanged); + InputElement.IsPointerOverProperty.Changed.AddClassHandler(OnIsPointerOverChanged); + } + + private static void OnTipChanged(Control control, AvaloniaPropertyChangedEventArgs args) + { + var newTip = args.GetNewValue(); + if (newTip == null || ReferenceEquals(newTip, AvaloniaProperty.UnsetValue)) + { + var state = GetState(control); + CancelPendingOpen(state); + CancelPendingClose(state); + ToolTip.SetIsOpen(control, false); + ToolTip.SetServiceEnabled(control, true); + return; + } + + var normalizedPlacement = NormalizePlacement(ToolTip.GetPlacement(control)); + if (ToolTip.GetPlacement(control) != normalizedPlacement) + ToolTip.SetPlacement(control, normalizedPlacement); + + ToolTip.SetServiceEnabled(control, false); + + if (newTip is ToolTip existingToolTip) + { + ApplyPointerClass(existingToolTip, normalizedPlacement); + return; + } + + var wrappedToolTip = new ToolTip { Content = newTip }; + ApplyPointerClass(wrappedToolTip, normalizedPlacement); + ToolTip.SetTip(control, wrappedToolTip); + } + + private static void OnToolTipOpening(Control control, CancelRoutedEventArgs _) => PrepareToolTip(control); + + private static void OnIsPointerOverChanged(Control control, AvaloniaPropertyChangedEventArgs args) + { + if (args.GetNewValue()) + { + OnPointerEntered(control); + return; + } + + OnPointerExited(control); + } + + private static void OnIsOpenChanged(Control control, AvaloniaPropertyChangedEventArgs args) + { + var wasOpen = args.GetOldValue(); + var isOpen = args.GetNewValue(); + if (wasOpen == isOpen) + return; + + var state = GetState(control); + + if (isOpen) + { + state.OpenedAt = DateTimeOffset.UtcNow; + CancelPendingOpen(state); + CancelPendingClose(state); + return; + } + + CancelPendingOpen(state); + CancelPendingClose(state); + ClearBubbleAnimationClasses(control); + } + + private static void OnPointerEntered(Control control) + { + if (!HasToolTip(control)) + return; + + var state = GetState(control); + var hadPendingClose = state.PendingCloseCts != null; + CancelPendingOpen(state); + CancelPendingClose(state); + + if (ToolTip.GetIsOpen(control)) + { + if (hadPendingClose && HasBubbleClass(control, BubbleAnimateOutClass)) + ApplyBubbleAnimationClass(control, animateIn: true); + return; + } + + var cts = new CancellationTokenSource(); + state.PendingOpenCts = cts; + _ = DeferredOpenAsync(control, state, HoverOpenDelay, cts.Token); + } + + private static void OnPointerExited(Control control) + { + if (!HasToolTip(control) && !ToolTip.GetIsOpen(control)) + return; + + var state = GetState(control); + CancelPendingOpen(state); + + var elapsed = DateTimeOffset.UtcNow - state.OpenedAt; + var remaining = MinimumVisibleTime - elapsed; + if (remaining < TimeSpan.Zero) + remaining = TimeSpan.Zero; + + CancelPendingClose(state); + var cts = new CancellationTokenSource(); + state.PendingCloseCts = cts; + _ = DeferredCloseAsync(control, state, remaining, cts.Token); + } + + private static void ApplyPointerClass(ToolTip toolTip, PlacementMode placement) + { + toolTip.Classes.Remove(BubblePointerLeftClass); + toolTip.Classes.Remove(BubblePointerMiddleClass); + toolTip.Classes.Remove(BubblePointerRightClass); + toolTip.Classes.Add(GetPointerClass(placement)); + } + + private static void ApplyPointerAnchorOffset(Control control, PlacementMode placement) + { + var controlCenterX = control.Bounds.Width / 2d; + var horizontalOffset = placement switch + { + PlacementMode.TopEdgeAlignedLeft => controlCenterX - TailCenterOffsetFromSide, + PlacementMode.TopEdgeAlignedRight => TailCenterOffsetFromSide - controlCenterX, + _ => 0d, + }; + + ToolTip.SetHorizontalOffset(control, horizontalOffset); + ToolTip.SetVerticalOffset(control, TooltipVerticalOffset); + } + + private static ToolTipState GetState(Control control) => ToolTipStates.GetOrCreateValue(control); + + private static bool HasToolTip(Control control) + { + var tip = ToolTip.GetTip(control); + return tip != null && !ReferenceEquals(tip, AvaloniaProperty.UnsetValue); + } + + private static void PrepareToolTip(Control control) + { + var normalizedPlacement = NormalizePlacement(ToolTip.GetPlacement(control)); + if (ToolTip.GetPlacement(control) != normalizedPlacement) + ToolTip.SetPlacement(control, normalizedPlacement); + + var toolTip = GetToolTipInstance(control); + if (toolTip == null) + return; + + ApplyPointerClass(toolTip, normalizedPlacement); + ApplyPointerAnchorOffset(control, normalizedPlacement); + } + + private static ToolTip? GetToolTipInstance(Control control) => + control.GetValue(ToolTipDiagnostics.ToolTipProperty) as ToolTip ?? ToolTip.GetTip(control) as ToolTip; + + private static bool HasBubbleClass(Control control, string className) + { + var toolTip = GetToolTipInstance(control); + return toolTip != null && toolTip.Classes.Contains(className); + } + + private static void ApplyBubbleAnimationClass(Control control, bool animateIn) + { + var toolTip = GetToolTipInstance(control); + if (toolTip == null) + return; + + toolTip.Classes.Remove(BubbleAnimateInClass); + toolTip.Classes.Remove(BubbleAnimateOutClass); + toolTip.Classes.Add(animateIn ? BubbleAnimateInClass : BubbleAnimateOutClass); + } + + private static void ClearBubbleAnimationClasses(Control control) + { + var toolTip = GetToolTipInstance(control); + if (toolTip == null) + return; + + toolTip.Classes.Remove(BubbleAnimateInClass); + toolTip.Classes.Remove(BubbleAnimateOutClass); + } + + private static void CancelPendingClose(ToolTipState state) + { + if (state.PendingCloseCts == null) + return; + + state.PendingCloseCts.Cancel(); + state.PendingCloseCts.Dispose(); + state.PendingCloseCts = null; + } + + private static void CancelPendingOpen(ToolTipState state) + { + if (state.PendingOpenCts == null) + return; + + state.PendingOpenCts.Cancel(); + state.PendingOpenCts.Dispose(); + state.PendingOpenCts = null; + } + + private static async Task DeferredOpenAsync(Control control, ToolTipState state, TimeSpan delay, CancellationToken cancellationToken) + { + try + { + await Task.Delay(delay, cancellationToken); + } + catch (TaskCanceledException) + { + return; + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + state.PendingOpenCts?.Dispose(); + state.PendingOpenCts = null; + + if (!control.IsPointerOver || ToolTip.GetIsOpen(control) || !HasToolTip(control)) + return; + + PrepareToolTip(control); + ApplyBubbleAnimationClass(control, animateIn: true); + ToolTip.SetIsOpen(control, true); + }); + } + + private static async Task DeferredCloseAsync(Control control, ToolTipState state, TimeSpan delay, CancellationToken cancellationToken) + { + try + { + if (delay > TimeSpan.Zero) + await Task.Delay(delay, cancellationToken); + } + catch (TaskCanceledException) + { + return; + } + + var shouldAnimateOut = false; + await Dispatcher.UIThread.InvokeAsync(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + if (control.IsPointerOver || !ToolTip.GetIsOpen(control)) + { + state.PendingCloseCts?.Dispose(); + state.PendingCloseCts = null; + return; + } + + shouldAnimateOut = true; + ApplyBubbleAnimationClass(control, animateIn: false); + }); + + if (!shouldAnimateOut) + return; + + try + { + await Task.Delay(CloseAnimationDuration, cancellationToken); + } + catch (TaskCanceledException) + { + return; + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (cancellationToken.IsCancellationRequested) + return; + + state.PendingCloseCts?.Dispose(); + state.PendingCloseCts = null; + if (control.IsPointerOver) + { + if (HasBubbleClass(control, BubbleAnimateOutClass)) + ApplyBubbleAnimationClass(control, animateIn: true); + return; + } + + ToolTip.SetIsOpen(control, false); + }); + } + + private static string GetPointerClass(PlacementMode placement) => + placement switch + { + PlacementMode.TopEdgeAlignedLeft => BubblePointerLeftClass, + PlacementMode.TopEdgeAlignedRight => BubblePointerRightClass, + _ => BubblePointerMiddleClass, + }; + + private static PlacementMode NormalizePlacement(PlacementMode placement) => + placement switch + { + PlacementMode.Left => PlacementMode.TopEdgeAlignedLeft, + PlacementMode.LeftEdgeAlignedTop => PlacementMode.TopEdgeAlignedLeft, + PlacementMode.LeftEdgeAlignedBottom => PlacementMode.TopEdgeAlignedLeft, + PlacementMode.TopEdgeAlignedLeft => PlacementMode.TopEdgeAlignedLeft, + PlacementMode.BottomEdgeAlignedLeft => PlacementMode.TopEdgeAlignedLeft, + PlacementMode.Right => PlacementMode.TopEdgeAlignedRight, + PlacementMode.RightEdgeAlignedTop => PlacementMode.TopEdgeAlignedRight, + PlacementMode.RightEdgeAlignedBottom => PlacementMode.TopEdgeAlignedRight, + PlacementMode.TopEdgeAlignedRight => PlacementMode.TopEdgeAlignedRight, + PlacementMode.BottomEdgeAlignedRight => PlacementMode.TopEdgeAlignedRight, + _ => PlacementMode.Top, + }; + + private sealed class ToolTipState + { + public DateTimeOffset OpenedAt { get; set; } + public CancellationTokenSource? PendingOpenCts { get; set; } + public CancellationTokenSource? PendingCloseCts { get; set; } + } +} diff --git a/WheelWizard/Views/BehaviorComponent/AspectGrid.axaml b/WheelWizard/Views/Components/AspectGrid.axaml similarity index 86% rename from WheelWizard/Views/BehaviorComponent/AspectGrid.axaml rename to WheelWizard/Views/Components/AspectGrid.axaml index 8cf0eb5f..c5354c7c 100644 --- a/WheelWizard/Views/BehaviorComponent/AspectGrid.axaml +++ b/WheelWizard/Views/Components/AspectGrid.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="WheelWizard.Views.BehaviorComponent.AspectGrid" + x:Class="WheelWizard.Views.Components.AspectGrid" SizeChanged="OnSizeChanged"> - \ No newline at end of file + diff --git a/WheelWizard/Views/BehaviorComponent/AspectGrid.axaml.cs b/WheelWizard/Views/Components/AspectGrid.axaml.cs similarity index 97% rename from WheelWizard/Views/BehaviorComponent/AspectGrid.axaml.cs rename to WheelWizard/Views/Components/AspectGrid.axaml.cs index 93145b55..da51dc21 100644 --- a/WheelWizard/Views/BehaviorComponent/AspectGrid.axaml.cs +++ b/WheelWizard/Views/Components/AspectGrid.axaml.cs @@ -1,7 +1,7 @@ using Avalonia; using Avalonia.Controls; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Components; public partial class AspectGrid : Grid { diff --git a/WheelWizard/Views/Components/WhWzLibrary/Badge.axaml b/WheelWizard/Views/Components/Badge.axaml similarity index 100% rename from WheelWizard/Views/Components/WhWzLibrary/Badge.axaml rename to WheelWizard/Views/Components/Badge.axaml diff --git a/WheelWizard/Views/Components/WhWzLibrary/Badge.axaml.cs b/WheelWizard/Views/Components/Badge.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/WhWzLibrary/Badge.axaml.cs rename to WheelWizard/Views/Components/Badge.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/Button.axaml b/WheelWizard/Views/Components/Button.axaml similarity index 99% rename from WheelWizard/Views/Components/StandardLibrary/Button.axaml rename to WheelWizard/Views/Components/Button.axaml index 94418edc..56f5222e 100644 --- a/WheelWizard/Views/Components/StandardLibrary/Button.axaml +++ b/WheelWizard/Views/Components/Button.axaml @@ -24,7 +24,7 @@ IconData="{StaticResource WheelIcon}" IconSize="25" ToolTip.Tip="Launch Dolphin" - ToolTip.Placement="Bottom" + ToolTip.Placement="Top" ToolTip.ShowDelay="50" HorizontalAlignment="Center" Margin="0,6,0,0" /> @@ -140,4 +140,4 @@ - \ No newline at end of file + diff --git a/WheelWizard/Views/Components/StandardLibrary/Button.axaml.cs b/WheelWizard/Views/Components/Button.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/Button.axaml.cs rename to WheelWizard/Views/Components/Button.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/EmptyPageInfo.axaml b/WheelWizard/Views/Components/EmptyPageInfo.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/EmptyPageInfo.axaml rename to WheelWizard/Views/Components/EmptyPageInfo.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/EmptyPageInfo.axaml.cs b/WheelWizard/Views/Components/EmptyPageInfo.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/EmptyPageInfo.axaml.cs rename to WheelWizard/Views/Components/EmptyPageInfo.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml b/WheelWizard/Views/Components/FormFieldLabel.axaml similarity index 89% rename from WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml rename to WheelWizard/Views/Components/FormFieldLabel.axaml index 30239ad0..81ee970c 100644 --- a/WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml +++ b/WheelWizard/Views/Components/FormFieldLabel.axaml @@ -10,6 +10,7 @@ - \ No newline at end of file + diff --git a/WheelWizard/Views/Components/StandardLibrary/LoadingIcon.axaml.cs b/WheelWizard/Views/Components/LoadingIcon.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/LoadingIcon.axaml.cs rename to WheelWizard/Views/Components/LoadingIcon.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/MemeNumberState.axaml b/WheelWizard/Views/Components/MemeNumberState.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MemeNumberState.axaml rename to WheelWizard/Views/Components/MemeNumberState.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/MemeNumberState.axaml.cs b/WheelWizard/Views/Components/MemeNumberState.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MemeNumberState.axaml.cs rename to WheelWizard/Views/Components/MemeNumberState.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/MultiColoredIcon.axaml b/WheelWizard/Views/Components/MultiColoredIcon.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MultiColoredIcon.axaml rename to WheelWizard/Views/Components/MultiColoredIcon.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/MultiColoredIcon.axaml.cs b/WheelWizard/Views/Components/MultiColoredIcon.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MultiColoredIcon.axaml.cs rename to WheelWizard/Views/Components/MultiColoredIcon.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/MultiIconRadioButton.axaml b/WheelWizard/Views/Components/MultiIconRadioButton.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MultiIconRadioButton.axaml rename to WheelWizard/Views/Components/MultiIconRadioButton.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/MultiIconRadioButton.axaml.cs b/WheelWizard/Views/Components/MultiIconRadioButton.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/MultiIconRadioButton.axaml.cs rename to WheelWizard/Views/Components/MultiIconRadioButton.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/OptionButton.axaml b/WheelWizard/Views/Components/OptionButton.axaml similarity index 97% rename from WheelWizard/Views/Components/StandardLibrary/OptionButton.axaml rename to WheelWizard/Views/Components/OptionButton.axaml index ef07671d..9356747a 100644 --- a/WheelWizard/Views/Components/StandardLibrary/OptionButton.axaml +++ b/WheelWizard/Views/Components/OptionButton.axaml @@ -1,7 +1,7 @@  + xmlns:behavior="clr-namespace:WheelWizard.Views.Patterns"> @@ -27,7 +27,7 @@ - + - + @@ -112,4 +112,4 @@ - \ No newline at end of file + diff --git a/WheelWizard/Views/Components/StandardLibrary/OptionButton.axaml.cs b/WheelWizard/Views/Components/OptionButton.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/OptionButton.axaml.cs rename to WheelWizard/Views/Components/OptionButton.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/PopupListButton.axaml b/WheelWizard/Views/Components/PopupListButton.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/PopupListButton.axaml rename to WheelWizard/Views/Components/PopupListButton.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/PopupListButton.axaml.cs b/WheelWizard/Views/Components/PopupListButton.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/PopupListButton.axaml.cs rename to WheelWizard/Views/Components/PopupListButton.axaml.cs diff --git a/WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml.cs b/WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml.cs deleted file mode 100644 index 596f382c..00000000 --- a/WheelWizard/Views/Components/StandardLibrary/FormFieldLabel.axaml.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Avalonia; -using Avalonia.Controls; - -namespace WheelWizard.Views.Components; - -public class FormFieldLabel : UserControl -{ - public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); // Add a default value here - - public string Text - { - get => GetValue(TextProperty); - set => SetValue(TextProperty, value); - } - - public static readonly StyledProperty TipTextProperty = AvaloniaProperty.Register(nameof(TipText)); // Add a default value here - - public string TipText - { - get => GetValue(TipTextProperty); - set => SetValue(TipTextProperty, value); - } -} diff --git a/WheelWizard/Views/Components/StandardLibrary/StateBox.axaml b/WheelWizard/Views/Components/StateBox.axaml similarity index 100% rename from WheelWizard/Views/Components/StandardLibrary/StateBox.axaml rename to WheelWizard/Views/Components/StateBox.axaml diff --git a/WheelWizard/Views/Components/StandardLibrary/StateBox.axaml.cs b/WheelWizard/Views/Components/StateBox.axaml.cs similarity index 99% rename from WheelWizard/Views/Components/StandardLibrary/StateBox.axaml.cs rename to WheelWizard/Views/Components/StateBox.axaml.cs index 632ef21b..9e7d7e5a 100644 --- a/WheelWizard/Views/Components/StandardLibrary/StateBox.axaml.cs +++ b/WheelWizard/Views/Components/StateBox.axaml.cs @@ -78,7 +78,7 @@ public StateBoxVariantType Variant public static readonly StyledProperty TipPlacementProperty = AvaloniaProperty.Register( nameof(TipPlacement), - PlacementMode.Right + PlacementMode.Top ); public PlacementMode TipPlacement diff --git a/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml b/WheelWizard/Views/Components/WheelTrail.axaml similarity index 100% rename from WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml rename to WheelWizard/Views/Components/WheelTrail.axaml diff --git a/WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml.cs b/WheelWizard/Views/Components/WheelTrail.axaml.cs similarity index 100% rename from WheelWizard/Views/Components/WhWzLibrary/WheelTrail.axaml.cs rename to WheelWizard/Views/Components/WheelTrail.axaml.cs diff --git a/WheelWizard/Views/Layout.axaml b/WheelWizard/Views/Layout.axaml index 97b94519..d2886b41 100644 --- a/WheelWizard/Views/Layout.axaml +++ b/WheelWizard/Views/Layout.axaml @@ -6,8 +6,8 @@ x:Class="WheelWizard.Views.Layout" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:pages="clr-namespace:WheelWizard.Views.Pages" - xmlns:settings="clr-namespace:WheelWizard.Views.Pages.Settings" xmlns:components="clr-namespace:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" Height="876" Width="656" WindowStartupLocation="CenterScreen" SystemDecorations="None" ExtendClientAreaToDecorationsHint="True" @@ -57,9 +57,11 @@ - - + + - - - - - - - - - - diff --git a/WheelWizard/Views/Layout.axaml.cs b/WheelWizard/Views/Layout.axaml.cs index 822be5b7..9c41e809 100644 --- a/WheelWizard/Views/Layout.axaml.cs +++ b/WheelWizard/Views/Layout.axaml.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using Avalonia; +using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -15,6 +16,7 @@ using WheelWizard.Utilities.RepeatedTasks; using WheelWizard.Views.Components; using WheelWizard.Views.Pages; +using WheelWizard.Views.Patterns; using WheelWizard.Views.Popups.Generic; using WheelWizard.WheelWizardData.Domain; using WheelWizard.WiiManagement; @@ -37,6 +39,16 @@ public partial class Layout : BaseWindow, IRepeatedTaskListener // testing builds since they are behind authentication walls. // but have fun with the beta button :) private const string TesterSecretPhrase = "WhenSonicInRR?"; + private static readonly TimeSpan PageSwapDuration = TimeSpan.FromMilliseconds(250); + private static readonly IPageTransition RoomsPageTransition = new CompositePageTransition + { + PageTransitions = + [ + new PageSlide { Duration = PageSwapDuration, Orientation = PageSlide.SlideAxis.Horizontal }, + new CrossFade { Duration = PageSwapDuration }, + ], + }; + private int _testerClickCount; private bool _testerPromptOpen; private IDisposable? _settingsSignalSubscription; @@ -128,8 +140,18 @@ private void OnSettingChanged(Setting setting) public void NavigateToPage(UserControl page) { + var oldPage = ContentArea.Content as Control; + var isRoomsToDetails = oldPage is RoomsPage && page is RoomDetailsPage; + var isDetailsToRooms = oldPage is RoomDetailsPage && page is RoomsPage; + + ContentArea.PageTransition = isRoomsToDetails || isDetailsToRooms ? RoomsPageTransition : null; + ContentArea.IsTransitionReversed = isDetailsToRooms; ContentArea.Content = page; + UpdateSidebarSelection(page); + } + private void UpdateSidebarSelection(UserControl page) + { // Update the IsChecked state of the SidebarRadioButtons foreach (var child in SidePanelButtons.Children) { diff --git a/WheelWizard/Views/Pages/FriendsPage.axaml b/WheelWizard/Views/Pages/FriendsPage.axaml index 5560178f..53f197d2 100644 --- a/WheelWizard/Views/Pages/FriendsPage.axaml +++ b/WheelWizard/Views/Pages/FriendsPage.axaml @@ -5,7 +5,7 @@ mc:Ignorable="d" d:DesignWidth="656" d:DesignHeight="876" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" - xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.BehaviorComponent" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" xmlns:domain="clr-namespace:WheelWizard.WiiManagement.GameLicense.Domain" x:Class="WheelWizard.Views.Pages.FriendsPage"> @@ -35,17 +35,17 @@ ToolTip.Tip="Add Friend" ToolTip.Placement="Top" ToolTip.ShowDelay="20" /> - - - + @@ -74,7 +74,7 @@ ScrollViewer.VerticalScrollBarVisibility="Auto"> - ValidateFriendCodeInput(newText)) + .SetWarningValidation((_, newText) => ValidateFriendCodeWarning(newText)) .ShowDialog(); if (inputFriendCode == null) @@ -230,13 +231,23 @@ private OperationResult ValidateFriendCodeInput(string? rawFriendCode) if (currentProfileId != 0 && currentProfileId == friendProfileId) return Fail("You cannot add your own friend code."); + return Ok(); + } + + private string? ValidateFriendCodeWarning(string? rawFriendCode) + { + var normalizedFriendCodeResult = NormalizeFriendCode(rawFriendCode ?? string.Empty); + if (normalizedFriendCodeResult.IsFailure) + return null; + + var friendProfileId = FriendCodeGenerator.FriendCodeToProfileId(normalizedFriendCodeResult.Value); var duplicateFriend = GameLicenseService.ActiveCurrentFriends.Any(friend => { var existingPid = FriendCodeGenerator.FriendCodeToProfileId(friend.FriendCode); return existingPid != 0 && existingPid == friendProfileId; }); - return duplicateFriend ? Fail("This friend is already in your list.") : Ok(); + return duplicateFriend ? "This friend is already in your list." : null; } private static OperationResult NormalizeFriendCode(string friendCode) diff --git a/WheelWizard/Views/Pages/HomePage.axaml b/WheelWizard/Views/Pages/HomePage.axaml index 59f0e2f7..62875b08 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml +++ b/WheelWizard/Views/Pages/HomePage.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" - xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.BehaviorComponent" + xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.Patterns" mc:Ignorable="d" d:DesignWidth="490" d:DesignHeight="830" ClipToBounds="False" x:Class="WheelWizard.Views.Pages.HomePage"> @@ -173,12 +173,13 @@ IsEnabled="True" IconData="{StaticResource DolphinIcon}" IconSize="30" - ToolTip.Tip="{x:Static lang:Common.Action_LaunchDolphin}" - ToolTip.Placement="Bottom" - ToolTip.ShowDelay="50" Height="40" Click="DolphinButton_OnClick" Width="100" Margin="0,6,0,0" /> + + - \ No newline at end of file + diff --git a/WheelWizard/Views/Pages/HomePage.axaml.cs b/WheelWizard/Views/Pages/HomePage.axaml.cs index ccf8386c..aa26a263 100644 --- a/WheelWizard/Views/Pages/HomePage.axaml.cs +++ b/WheelWizard/Views/Pages/HomePage.axaml.cs @@ -12,7 +12,6 @@ using WheelWizard.Settings; using WheelWizard.Shared.DependencyInjection; using WheelWizard.Views.Components; -using WheelWizard.Views.Pages.Settings; using Button = WheelWizard.Views.Components.Button; namespace WheelWizard.Views.Pages; diff --git a/WheelWizard/Views/Pages/KitchenSinkPage.axaml b/WheelWizard/Views/Pages/KitchenSinkPage.axaml new file mode 100644 index 00000000..d9d0743b --- /dev/null +++ b/WheelWizard/Views/Pages/KitchenSinkPage.axaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/KitchenSinkPage.axaml.cs b/WheelWizard/Views/Pages/KitchenSinkPage.axaml.cs new file mode 100644 index 00000000..78a48a41 --- /dev/null +++ b/WheelWizard/Views/Pages/KitchenSinkPage.axaml.cs @@ -0,0 +1,233 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Layout; +using WheelWizard.Views.Components; +using WheelWizard.Views.Pages.KitchenSink; + +namespace WheelWizard.Views.Pages; + +public partial class KitchenSinkPage : UserControlBase +{ + private readonly SectionDefinition[] _sections = + [ + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + SectionDefinition.Create(), + ]; + + // Add more configurable section groups here. + private readonly SectionCollectionDefinition[] _sectionCollections = + [ + SectionCollectionDefinition.Create( + "Basic Components", + typeof(KitchenSinkTextStylesPage), + typeof(KitchenSinkToggleButtonsPage), + typeof(KitchenSinkInputFieldsPage), + typeof(KitchenSinkDropdownsPage), + typeof(KitchenSinkButtonsPage), + typeof(KitchenSinkIconLabelsPage), + typeof(KitchenSinkStateBoxesPage), + typeof(KitchenSinkIconsPage) + ), + ]; + + private readonly Dictionary _allSectionContainersById = []; + private readonly List _allSectionBorders = []; + private Border? _singleSectionBorder; + private bool _isInitializing; + private bool _useNeutral900Blocks = true; + + public KitchenSinkPage() + { + InitializeComponent(); + BuildAllSections(); + PopulateSections(); + ApplyBlockBackgroundMode(); + } + + private void BuildAllSections() + { + _allSectionContainersById.Clear(); + _allSectionBorders.Clear(); + AllSectionsContainer.Children.Clear(); + + foreach (var section in _sections) + { + var sectionView = section.CreatePage(); + var sectionContainer = CreateSectionContainer(section.Label, section.Tooltip, sectionView, out var sectionBorder); + _allSectionContainersById[section.Id] = sectionContainer; + _allSectionBorders.Add(sectionBorder); + AllSectionsContainer.Children.Add(sectionContainer); + } + } + + private static Border CreateSectionContainer( + string sectionName, + string? sectionTooltip, + Control sectionContent, + out Border sectionBorder + ) + { + var header = new FormFieldLabel { Text = sectionName, TipText = sectionTooltip ?? string.Empty }; + var divider = new Border(); + divider.Classes.Add("KitchenSinkSectionDivider"); + var body = new StackPanel { Spacing = 8 }; + body.Children.Add(header); + body.Children.Add(divider); + body.Children.Add(sectionContent); + + sectionBorder = new Border { Child = body }; + sectionBorder.Classes.Add("KitchenSinkSectionBlock"); + sectionBorder.VerticalAlignment = VerticalAlignment.Top; + sectionBorder.HorizontalAlignment = HorizontalAlignment.Stretch; + return sectionBorder; + } + + private void PopulateSections() + { + _isInitializing = true; + SectionDropdown.Items.Clear(); + + foreach (var collection in _sectionCollections) + SectionDropdown.Items.Add(SectionDropdownItem.ForCollection(collection)); + + foreach (var section in _sections) + SectionDropdown.Items.Add(SectionDropdownItem.ForSection(section)); + + SectionDropdown.SelectedIndex = SectionDropdown.Items.Count > 0 ? 0 : -1; + _isInitializing = false; + + ApplySelectedSection(); + } + + private void SectionDropdown_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (_isInitializing) + return; + + ApplySelectedSection(); + } + + private void BackgroundSwitch_OnIsCheckedChanged(object? sender, RoutedEventArgs e) + { + _useNeutral900Blocks = BackgroundSwitch.IsChecked == true; + ApplyBlockBackgroundMode(); + } + + private void ApplySelectedSection() + { + var selectedItem = SectionDropdown.SelectedItem as SectionDropdownItem; + if (selectedItem == null) + { + AllSectionsScrollViewer.IsVisible = false; + SectionContent.IsVisible = false; + SectionContent.Content = null; + _singleSectionBorder = null; + return; + } + + if (selectedItem.Collection is { } collection) + { + ShowCollection(collection); + return; + } + + if (string.IsNullOrWhiteSpace(selectedItem.SectionId)) + return; + + ShowSingleSection(selectedItem.SectionId); + } + + private void ShowCollection(SectionCollectionDefinition collection) + { + foreach (var section in _sections) + { + if (_allSectionContainersById.TryGetValue(section.Id, out var sectionContainer)) + sectionContainer.IsVisible = collection.Contains(section.Id); + } + + AllSectionsScrollViewer.IsVisible = true; + SectionContent.IsVisible = false; + _singleSectionBorder = null; + SectionContent.Content = null; + + ApplyBlockBackgroundMode(); + } + + private void ShowSingleSection(string sectionId) + { + var sectionIndex = Array.FindIndex(_sections, x => x.Id == sectionId); + if (sectionIndex < 0) + { + SectionContent.Content = null; + return; + } + + var section = _sections[sectionIndex]; + var sectionContainer = CreateSectionContainer(section.Label, section.Tooltip, section.CreatePage(), out var sectionBorder); + _singleSectionBorder = sectionBorder; + SectionContent.Content = new ScrollViewer + { + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Top, + Content = sectionContainer, + }; + + AllSectionsScrollViewer.IsVisible = false; + SectionContent.IsVisible = true; + + ApplyBlockBackgroundMode(); + } + + private void ApplyBlockBackgroundMode() + { + foreach (var border in _allSectionBorders) + ApplyBackgroundClass(border); + + if (_singleSectionBorder != null) + ApplyBackgroundClass(_singleSectionBorder); + } + + private void ApplyBackgroundClass(Border border) + { + border.Classes.Set("BlockBackground900", _useNeutral900Blocks); + } + + private readonly record struct SectionDefinition(string Id, string Label, string? Tooltip, Func CreatePage) + { + public static SectionDefinition Create() + where T : KitchenSinkSectionPageBase, new() + { + var metadataInstance = new T(); + return new(typeof(T).Name, metadataInstance.SectionName, metadataInstance.SectionTooltip, () => new T()); + } + } + + private readonly record struct SectionCollectionDefinition(string Label, HashSet SectionIds) + { + public bool Contains(string sectionId) => SectionIds.Contains(sectionId); + + public static SectionCollectionDefinition Create(string label, params Type[] sectionTypes) + { + var sectionIds = sectionTypes.Select(static sectionType => sectionType.Name).ToHashSet(StringComparer.Ordinal); + return new(label, sectionIds); + } + } + + private sealed record SectionDropdownItem(string Label, string? SectionId, SectionCollectionDefinition? Collection) + { + public static SectionDropdownItem ForCollection(SectionCollectionDefinition collection) => new(collection.Label, null, collection); + + public static SectionDropdownItem ForSection(SectionDefinition section) => new(section.Label, section.Id, null); + + public override string ToString() => Label; + } +} diff --git a/WheelWizard/Views/Pages/LeaderboardPage.axaml b/WheelWizard/Views/Pages/LeaderboardPage.axaml index 0d2b1872..38b94aa2 100644 --- a/WheelWizard/Views/Pages/LeaderboardPage.axaml +++ b/WheelWizard/Views/Pages/LeaderboardPage.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" xmlns:pages="clr-namespace:WheelWizard.Views.Pages" mc:Ignorable="d" d:DesignWidth="477" @@ -46,21 +47,21 @@ - - - - @@ -189,7 +190,7 @@ RowDefinitions="Auto,16" VerticalAlignment="Center" ColumnDefinitions="*,1.1*,*"> - - + @@ -214,10 +215,10 @@ - - + + - - + @@ -243,10 +244,10 @@ - - + + - - + @@ -271,8 +272,8 @@ - - + + @@ -280,7 +281,7 @@ - - + @@ -303,8 +304,8 @@ - - + + diff --git a/WheelWizard/Views/Pages/MiiListPage.axaml b/WheelWizard/Views/Pages/MiiListPage.axaml index 0029360e..6240715a 100644 --- a/WheelWizard/Views/Pages/MiiListPage.axaml +++ b/WheelWizard/Views/Pages/MiiListPage.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:components="clr-namespace:WheelWizard.Views.Components" - xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.BehaviorComponent" + xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.Patterns" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" mc:Ignorable="d" d:DesignWidth="456" d:DesignHeight="776" x:Class="WheelWizard.Views.Pages.MiiListPage"> @@ -83,9 +83,9 @@ - + - \ No newline at end of file + diff --git a/WheelWizard/Views/Pages/MiiListPage.axaml.cs b/WheelWizard/Views/Pages/MiiListPage.axaml.cs index 1b81890d..90b70267 100644 --- a/WheelWizard/Views/Pages/MiiListPage.axaml.cs +++ b/WheelWizard/Views/Pages/MiiListPage.axaml.cs @@ -13,6 +13,7 @@ using WheelWizard.Shared.DependencyInjection; using WheelWizard.Shared.MessageTranslations; using WheelWizard.Views.Components; +using WheelWizard.Views.Patterns; using WheelWizard.Views.Popups.Generic; using WheelWizard.Views.Popups.MiiManagement; using WheelWizard.WiiManagement; diff --git a/WheelWizard/Views/Pages/ModsPage.axaml b/WheelWizard/Views/Pages/ModsPage.axaml index a7c7ff0a..2dfc4105 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml +++ b/WheelWizard/Views/Pages/ModsPage.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" xmlns:pages="clr-namespace:WheelWizard.Views.Pages" mc:Ignorable="d" d:DesignWidth="656" d:DesignHeight="876" x:Class="WheelWizard.Views.Pages.ModsPage" @@ -198,17 +199,18 @@ - + HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" + VerticalContentAlignment="Top"> + - + - + @@ -229,4 +231,4 @@ - \ No newline at end of file + diff --git a/WheelWizard/Views/Pages/ModsPage.axaml.cs b/WheelWizard/Views/Pages/ModsPage.axaml.cs index e7fe00cd..a051c7b2 100644 --- a/WheelWizard/Views/Pages/ModsPage.axaml.cs +++ b/WheelWizard/Views/Pages/ModsPage.axaml.cs @@ -68,10 +68,14 @@ public ModsPage() { InitializeComponent(); DataContext = this; + Focusable = true; ModManager.PropertyChanged += OnModsChanged; ModManager.ReloadAsync(); SetModsViewVariant(); + // Apply priority edits as soon as the user clicks anywhere outside the textbox. + AddHandler(PointerPressedEvent, OnPagePointerPressed, RoutingStrategies.Tunnel, true); + // Wire up drag-and-drop pointer tracking PointerMoved += OnDragPointerMoved; PointerReleased += OnDragPointerReleased; @@ -82,6 +86,8 @@ private void OnModsChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(ModManager.Mods)) OnModsChanged(); + else if (e.PropertyName == nameof(Mod.IsEnabled)) + UpdateEnableAllCheckboxState(); } private void OnModsChanged() @@ -91,7 +97,12 @@ private void OnModsChanged() ListItemCount.Text = ModManager.Mods.Count.ToString(); OnPropertyChanged(nameof(Mods)); - HasMods = Mods.Count > 0; + HasMods = ModManager.Mods.Count > 0; + UpdateEnableAllCheckboxState(); + } + + private void UpdateEnableAllCheckboxState() + { EnableAllCheckbox.IsChecked = !ModManager.Mods.Select(mod => mod.IsEnabled).Contains(false); } @@ -258,6 +269,21 @@ private void PriorityText_OnKeyDown(object? sender, KeyEventArgs e) ViewUtils.FindParent(e.Source)?.Focus(); } + private void OnPagePointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + if (e.Source is TextBox || ViewUtils.FindParent(e.Source) != null) + return; + + var clickedControl = e.Source as Control ?? ViewUtils.FindParent(e.Source); + if (clickedControl?.Focusable == true) + clickedControl.Focus(NavigationMethod.Pointer, e.KeyModifiers); + else + Focus(NavigationMethod.Pointer, e.KeyModifiers); + } + #region Drag and Drop private void DragHandle_PointerPressed(object? sender, PointerPressedEventArgs e) diff --git a/WheelWizard/Views/Pages/RoomDetailsPage.axaml b/WheelWizard/Views/Pages/RoomDetailsPage.axaml index 0c2a78e4..f1271c64 100644 --- a/WheelWizard/Views/Pages/RoomDetailsPage.axaml +++ b/WheelWizard/Views/Pages/RoomDetailsPage.axaml @@ -5,6 +5,7 @@ mc:Ignorable="d" d:DesignHeight="876" d:DesignWidth="456" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" xmlns:pages="clr-namespace:WheelWizard.Views.Pages" x:Class="WheelWizard.Views.Pages.RoomDetailsPage" x:DataType="pages:RoomDetailsPage"> @@ -86,7 +87,7 @@ SelectionChanged="PlayerView_SelectionChanged"> - @@ -23,11 +24,11 @@ - - @@ -139,7 +140,7 @@ SelectionChanged="PlayerView_SelectionChanged"> - + x:Class="WheelWizard.Views.Pages.SettingsPage"> diff --git a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs b/WheelWizard/Views/Pages/SettingsPage.axaml.cs similarity index 76% rename from WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs rename to WheelWizard/Views/Pages/SettingsPage.axaml.cs index 2bde8008..0e9ba539 100644 --- a/WheelWizard/Views/Pages/Settings/SettingsPage.axaml.cs +++ b/WheelWizard/Views/Pages/SettingsPage.axaml.cs @@ -1,8 +1,9 @@ using Avalonia.Controls; using Avalonia.Interactivity; +using WheelWizard.Views.Pages.Settings; using WheelWizard.Views.Popups; -namespace WheelWizard.Views.Pages.Settings; +namespace WheelWizard.Views.Pages; public partial class SettingsPage : UserControlBase { @@ -25,9 +26,9 @@ private void TopBarRadio_OnClick(object? sender, RoutedEventArgs e) if (sender is not RadioButton radioButton) return; - // As long as the Ks... files are next to this file, it works. - var namespaceName = GetType().Namespace; - var typeName = $"{namespaceName}.{radioButton.Tag}"; + // Settings sub-pages stay in the nested Settings namespace. + var settingsSubPagesNamespace = typeof(WhWzSettings).Namespace; + var typeName = $"{settingsSubPagesNamespace}.{radioButton.Tag}"; var type = Type.GetType(typeName); if (type == null || !typeof(UserControl).IsAssignableFrom(type)) return; diff --git a/WheelWizard/Views/Pages/UserProfilePage.axaml b/WheelWizard/Views/Pages/UserProfilePage.axaml index f4f8fcfc..9ef72528 100644 --- a/WheelWizard/Views/Pages/UserProfilePage.axaml +++ b/WheelWizard/Views/Pages/UserProfilePage.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="656" d:DesignHeight="876" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" - xmlns:behavior="clr-namespace:WheelWizard.Views.BehaviorComponent" + xmlns:behavior="clr-namespace:WheelWizard.Views.Patterns" xmlns:components="clr-namespace:WheelWizard.Views.Components" xmlns:animation="clr-namespace:Avalonia.Animation;assembly=Avalonia.Base" xmlns:miiVars="using:WheelWizard.MiiImages.Domain" @@ -78,7 +78,7 @@ - - + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml new file mode 100644 index 00000000..717aa57e --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml.cs new file mode 100644 index 00000000..addf7ef7 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkButtonsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkButtonsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Buttons"; + public override string? SectionTooltip => null; + + public KitchenSinkButtonsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml new file mode 100644 index 00000000..ea524fab --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml.cs new file mode 100644 index 00000000..ed2c866d --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkDropdownsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkDropdownsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Dropdowns"; + public override string? SectionTooltip => null; + + public KitchenSinkDropdownsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml new file mode 100644 index 00000000..cb1747af --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml.cs new file mode 100644 index 00000000..ca8b3cf1 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconLabelsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkIconLabelsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Icon Labels"; + public override string? SectionTooltip => null; + + public KitchenSinkIconLabelsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml new file mode 100644 index 00000000..e2295067 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml.cs new file mode 100644 index 00000000..dc6562b3 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkIconsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkIconsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Icons"; + public override string? SectionTooltip => null; + + public KitchenSinkIconsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml new file mode 100644 index 00000000..dfa75cd3 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml.cs new file mode 100644 index 00000000..0204f3d2 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkInputFieldsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkInputFieldsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Input Fields"; + public override string? SectionTooltip => null; + + public KitchenSinkInputFieldsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkSectionPageBase.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkSectionPageBase.cs new file mode 100644 index 00000000..64863b7e --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkSectionPageBase.cs @@ -0,0 +1,13 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public interface IKitchenSinkSection +{ + string SectionName { get; } + string? SectionTooltip { get; } +} + +public abstract class KitchenSinkSectionPageBase : UserControlBase, IKitchenSinkSection +{ + public abstract string SectionName { get; } + public virtual string? SectionTooltip => null; +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml new file mode 100644 index 00000000..7a2f5839 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml.cs new file mode 100644 index 00000000..7b75436f --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkStateBoxesPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkStateBoxesPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "State Boxes"; + public override string? SectionTooltip => null; + + public KitchenSinkStateBoxesPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml new file mode 100644 index 00000000..3be42018 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml.cs new file mode 100644 index 00000000..3f0b8f34 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkTextStylesPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkTextStylesPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Text Styles + Tooltips"; + public override string? SectionTooltip => null; + + public KitchenSinkTextStylesPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml new file mode 100644 index 00000000..ded94299 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml.cs b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml.cs new file mode 100644 index 00000000..4cf64052 --- /dev/null +++ b/WheelWizard/Views/Pages/kitchenSink/KitchenSinkToggleButtonsPage.axaml.cs @@ -0,0 +1,12 @@ +namespace WheelWizard.Views.Pages.KitchenSink; + +public partial class KitchenSinkToggleButtonsPage : KitchenSinkSectionPageBase +{ + public override string SectionName => "Toggle Buttons"; + public override string? SectionTooltip => null; + + public KitchenSinkToggleButtonsPage() + { + InitializeComponent(); + } +} diff --git a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml b/WheelWizard/Views/Patterns/CurrentUserProfile.axaml similarity index 96% rename from WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml rename to WheelWizard/Views/Patterns/CurrentUserProfile.axaml index 69369396..ac759fb2 100644 --- a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml +++ b/WheelWizard/Views/Patterns/CurrentUserProfile.axaml @@ -1,8 +1,8 @@  diff --git a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs b/WheelWizard/Views/Patterns/CurrentUserProfile.axaml.cs similarity index 97% rename from WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs rename to WheelWizard/Views/Patterns/CurrentUserProfile.axaml.cs index 06de1dcb..fdc8782c 100644 --- a/WheelWizard/Views/BehaviorComponent/CurrentUserProfile.axaml.cs +++ b/WheelWizard/Views/Patterns/CurrentUserProfile.axaml.cs @@ -8,7 +8,7 @@ using WheelWizard.WiiManagement.GameLicense; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Patterns; public partial class CurrentUserProfile : UserControlBase { diff --git a/WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml b/WheelWizard/Views/Patterns/FeedbackTextBox.axaml similarity index 72% rename from WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml rename to WheelWizard/Views/Patterns/FeedbackTextBox.axaml index c67bb8af..45b26ae5 100644 --- a/WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml +++ b/WheelWizard/Views/Patterns/FeedbackTextBox.axaml @@ -3,8 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="100" - x:Class="WheelWizard.Views.BehaviorComponent.FeedbackTextBox" - xmlns:behaviorComp="clr-namespace:WheelWizard.Views.BehaviorComponent" + x:Class="WheelWizard.Views.Patterns.FeedbackTextBox" + xmlns:behaviorComp="clr-namespace:WheelWizard.Views.Patterns" xmlns:components="clr-namespace:WheelWizard.Views.Components" x:DataType="behaviorComp:FeedbackTextBox" > @@ -19,6 +19,12 @@ IconData="{StaticResource WarningTriangle}" IconSize="15" FontSize="13" Margin="4,4,0,0" /> + diff --git a/WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml.cs b/WheelWizard/Views/Patterns/FeedbackTextBox.axaml.cs similarity index 71% rename from WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml.cs rename to WheelWizard/Views/Patterns/FeedbackTextBox.axaml.cs index 8e82392a..a002d004 100644 --- a/WheelWizard/Views/BehaviorComponent/FeedbackTextBox.axaml.cs +++ b/WheelWizard/Views/Patterns/FeedbackTextBox.axaml.cs @@ -2,7 +2,7 @@ using Avalonia.Controls; using Avalonia.Interactivity; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Patterns; public partial class FeedbackTextBox : UserControlBase { @@ -17,6 +17,10 @@ public partial class FeedbackTextBox : UserControlBase nameof(ErrorMessage) ); + public static readonly StyledProperty WarningMessageProperty = AvaloniaProperty.Register( + nameof(WarningMessage) + ); + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register( @@ -51,6 +55,12 @@ public string ErrorMessage set => SetValue(ErrorMessageProperty, value); } + public string WarningMessage + { + get => GetValue(WarningMessageProperty); + set => SetValue(WarningMessageProperty, value); + } + public string Text { get => GetValue(TextProperty); @@ -89,6 +99,7 @@ public FeedbackTextBox() DataContext = this; InputField.TextChanged += (_, _) => RaiseEvent(new TextChangedEventArgs(TextChangedEvent, this)); + UpdateValidationState(hasError: !string.IsNullOrWhiteSpace(ErrorMessage), hasWarning: !string.IsNullOrWhiteSpace(WarningMessage)); // If there is uses for more other events, then we can always add them } @@ -100,16 +111,26 @@ private void UpdateStyleClasses(TextBoxVariantType variant) InputField.Classes.Remove("dark"); } - private void UpdateErrorState(bool hasError) + private void UpdateValidationState(bool hasError, bool hasWarning) { - if (!hasError) + if (hasError) + { + if (!InputField.Classes.Contains("error")) + InputField.Classes.Add("error"); + InputField.Classes.Remove("warning"); + return; + } + + InputField.Classes.Remove("error"); + + if (!hasWarning) { - InputField.Classes.Remove("error"); + InputField.Classes.Remove("warning"); return; } - if (!InputField.Classes.Contains("error")) - InputField.Classes.Add("error"); + if (!InputField.Classes.Contains("warning")) + InputField.Classes.Add("warning"); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -119,7 +140,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == VariantProperty) UpdateStyleClasses(change.GetNewValue()); - if (change.Property == ErrorMessageProperty) - UpdateErrorState(hasError: !string.IsNullOrWhiteSpace(change.GetNewValue())); + if (change.Property == ErrorMessageProperty || change.Property == WarningMessageProperty) + UpdateValidationState( + hasError: !string.IsNullOrWhiteSpace(ErrorMessage), + hasWarning: !string.IsNullOrWhiteSpace(WarningMessage) + ); } } diff --git a/WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml b/WheelWizard/Views/Patterns/FriendsListItem.axaml similarity index 91% rename from WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml rename to WheelWizard/Views/Patterns/FriendsListItem.axaml index 184682ea..6b363241 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml +++ b/WheelWizard/Views/Patterns/FriendsListItem.axaml @@ -3,38 +3,39 @@ xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="clr-namespace:WheelWizard.Views.Components" xmlns:miiVars="using:WheelWizard.MiiImages.Domain" - xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.BehaviorComponent"> + xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.Patterns" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns"> - - - - - - - - - diff --git a/WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml.cs b/WheelWizard/Views/Patterns/FriendsListItem.axaml.cs similarity index 97% rename from WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml.cs rename to WheelWizard/Views/Patterns/FriendsListItem.axaml.cs index 3a14bd0a..0926fc73 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/FriendsListItem.axaml.cs +++ b/WheelWizard/Views/Patterns/FriendsListItem.axaml.cs @@ -1,11 +1,12 @@ -using Avalonia; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Interactivity; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; +using Badge = WheelWizard.Views.Components.Badge; -namespace WheelWizard.Views.Components; +namespace WheelWizard.Views.Patterns; public class FriendsListItem : TemplatedControl { diff --git a/WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml b/WheelWizard/Views/Patterns/GridModPanel.axaml similarity index 95% rename from WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml rename to WheelWizard/Views/Patterns/GridModPanel.axaml index da766fd9..217a4d6a 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml +++ b/WheelWizard/Views/Patterns/GridModPanel.axaml @@ -3,10 +3,11 @@ xmlns:components="clr-namespace:WheelWizard.Views.Components" xmlns:pages="clr-namespace:WheelWizard.Views.Pages" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" - x:Class="WheelWizard.Views.Components.GridModPanel" + x:Class="WheelWizard.Views.Patterns.GridModPanel" x:DataType="pages:ModListItem"> - @@ -83,7 +84,7 @@ - diff --git a/WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml.cs b/WheelWizard/Views/Patterns/GridModPanel.axaml.cs similarity index 64% rename from WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml.cs rename to WheelWizard/Views/Patterns/GridModPanel.axaml.cs index 250be010..effc33cc 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/GridModPanel.axaml.cs +++ b/WheelWizard/Views/Patterns/GridModPanel.axaml.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -6,10 +7,14 @@ using WheelWizard.GameBanana; using WheelWizard.Views.Pages; -namespace WheelWizard.Views.Components; +namespace WheelWizard.Views.Patterns; public partial class GridModPanel : UserControl { + private static readonly HttpClient s_httpClient = new(); + private static readonly ConcurrentDictionary s_imageCache = new(); + private int? _currentModId; + public GridModPanel() { InitializeComponent(); @@ -18,15 +23,39 @@ public GridModPanel() private void OnDataContextChanged(object? sender, EventArgs e) { - // Reset image state when DataContext changes + if (DataContext is not ModListItem item) + { + _currentModId = null; + ModImage.Source = null; + PlaceholderIcon.IsVisible = true; + return; + } + + var modId = item.Mod.ModID; + _currentModId = modId; + + if (modId <= 0) + { + ModImage.Source = null; + PlaceholderIcon.IsVisible = true; + return; + } + + if (s_imageCache.TryGetValue(modId, out var cachedImage)) + { + ModImage.Source = cachedImage; + PlaceholderIcon.IsVisible = false; + return; + } + ModImage.Source = null; PlaceholderIcon.IsVisible = true; - LoadModImageAsync(); + LoadModImageAsync(modId); } - private async void LoadModImageAsync() + private async void LoadModImageAsync(int modId) { - if (DataContext is not ModListItem item || item.Mod.ModID <= 0) + if (modId <= 0) return; try @@ -35,7 +64,7 @@ private async void LoadModImageAsync() if (gameBananaService == null) return; - var result = await gameBananaService.GetModDetails(item.Mod.ModID); + var result = await gameBananaService.GetModDetails(modId); if (!result.IsSuccess || result.Value.PreviewMedia?.Images == null || result.Value.PreviewMedia.Images.Count == 0) return; @@ -43,15 +72,22 @@ private async void LoadModImageAsync() // Prefer smaller 220px thumbnail for grid cards, fall back to full size var imageUrl = image.File220 != null ? $"{image.BaseUrl}/{image.File220}" : $"{image.BaseUrl}/{image.File}"; - using var httpClient = new HttpClient(); - var response = await httpClient.GetAsync(imageUrl); + var response = await s_httpClient.GetAsync(imageUrl); response.EnsureSuccessStatusCode(); await using var stream = await response.Content.ReadAsStreamAsync(); var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream); memoryStream.Position = 0; - ModImage.Source = new Bitmap(memoryStream); + + var bitmap = new Bitmap(memoryStream); + if (!s_imageCache.TryAdd(modId, bitmap)) + bitmap.Dispose(); + + if (_currentModId != modId) + return; + + ModImage.Source = s_imageCache[modId]; PlaceholderIcon.IsVisible = false; } catch diff --git a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml b/WheelWizard/Views/Patterns/LeaderboardPodiumCard.axaml similarity index 93% rename from WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml rename to WheelWizard/Views/Patterns/LeaderboardPodiumCard.axaml index 41938974..7bc72aa2 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/LeaderboardPodiumCard.axaml +++ b/WheelWizard/Views/Patterns/LeaderboardPodiumCard.axaml @@ -1,12 +1,13 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + diff --git a/WheelWizard/Views/Components/WhWzLibrary/MiiBlock.axaml.cs b/WheelWizard/Views/Patterns/MiiBlock.axaml.cs similarity index 98% rename from WheelWizard/Views/Components/WhWzLibrary/MiiBlock.axaml.cs rename to WheelWizard/Views/Patterns/MiiBlock.axaml.cs index 129cf05c..c55f1c76 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/MiiBlock.axaml.cs +++ b/WheelWizard/Views/Patterns/MiiBlock.axaml.cs @@ -5,12 +5,11 @@ using Avalonia.Interactivity; using WheelWizard.MiiImages; using WheelWizard.MiiImages.Domain; -using WheelWizard.Views.BehaviorComponent; using WheelWizard.WiiManagement; using WheelWizard.WiiManagement.MiiManagement; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; -namespace WheelWizard.Views.Components; +namespace WheelWizard.Views.Patterns; public class MiiBlock : RadioButton { diff --git a/WheelWizard/Views/BehaviorComponent/MiiImages/BaseMiiImage.cs b/WheelWizard/Views/Patterns/MiiImages/BaseMiiImage.cs similarity index 99% rename from WheelWizard/Views/BehaviorComponent/MiiImages/BaseMiiImage.cs rename to WheelWizard/Views/Patterns/MiiImages/BaseMiiImage.cs index e617028d..41dadd67 100644 --- a/WheelWizard/Views/BehaviorComponent/MiiImages/BaseMiiImage.cs +++ b/WheelWizard/Views/Patterns/MiiImages/BaseMiiImage.cs @@ -7,7 +7,7 @@ using WheelWizard.Shared.DependencyInjection; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Patterns; public abstract class BaseMiiImage : UserControlBase, INotifyPropertyChanged { diff --git a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiCarousel.axaml b/WheelWizard/Views/Patterns/MiiImages/MiiCarousel.axaml similarity index 92% rename from WheelWizard/Views/BehaviorComponent/MiiImages/MiiCarousel.axaml rename to WheelWizard/Views/Patterns/MiiImages/MiiCarousel.axaml index 708d5a6a..39382036 100644 --- a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiCarousel.axaml +++ b/WheelWizard/Views/Patterns/MiiImages/MiiCarousel.axaml @@ -3,16 +3,16 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - xmlns:behaviorComp="clr-namespace:WheelWizard.Views.BehaviorComponent" + xmlns:behaviorComp="clr-namespace:WheelWizard.Views.Patterns" xmlns:components="clr-namespace:WheelWizard.Views.Components" xmlns:converters="clr-namespace:WheelWizard.Views.Converters" - x:Class="WheelWizard.Views.BehaviorComponent.MiiCarousel" x:Name="Self"> + x:Class="WheelWizard.Views.Patterns.MiiCarousel" x:Name="Self"> - - + - - - \ No newline at end of file + + diff --git a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoader.axaml.cs b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoader.axaml.cs similarity index 98% rename from WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoader.axaml.cs rename to WheelWizard/Views/Patterns/MiiImages/MiiImageLoader.axaml.cs index eef04bec..fbbfa04a 100644 --- a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoader.axaml.cs +++ b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoader.axaml.cs @@ -5,7 +5,7 @@ using WheelWizard.MiiImages.Domain; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Patterns; public partial class MiiImageLoader : BaseMiiImage { diff --git a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml similarity index 93% rename from WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml rename to WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml index 808a2d62..02ee9b18 100644 --- a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml +++ b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml @@ -3,13 +3,13 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400" - x:Class="WheelWizard.Views.BehaviorComponent.MiiImageLoaderWithHover" - xmlns:behaviorComp="clr-namespace:WheelWizard.Views.BehaviorComponent" + x:Class="WheelWizard.Views.Patterns.MiiImageLoaderWithHover" + xmlns:behaviorComp="clr-namespace:WheelWizard.Views.Patterns" xmlns:components="clr-namespace:WheelWizard.Views.Components" xmlns:conv="clr-namespace:WheelWizard.Views.Converters" x:DataType="behaviorComp:MiiImageLoaderWithHover" x:Name="Self" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> - - + diff --git a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml.cs b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml.cs similarity index 99% rename from WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml.cs rename to WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml.cs index 16d2d4bd..c30ff6f9 100644 --- a/WheelWizard/Views/BehaviorComponent/MiiImages/MiiImageLoaderWithHover.axaml.cs +++ b/WheelWizard/Views/Patterns/MiiImages/MiiImageLoaderWithHover.axaml.cs @@ -5,7 +5,7 @@ using WheelWizard.MiiImages.Domain; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; -namespace WheelWizard.Views.BehaviorComponent; +namespace WheelWizard.Views.Patterns; public partial class MiiImageLoaderWithHover : BaseMiiImage { diff --git a/WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml b/WheelWizard/Views/Patterns/ModBrowserListItem.axaml similarity index 91% rename from WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml rename to WheelWizard/Views/Patterns/ModBrowserListItem.axaml index 1487b2e6..1d8014e6 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml +++ b/WheelWizard/Views/Patterns/ModBrowserListItem.axaml @@ -1,33 +1,34 @@ + xmlns:components="using:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns"> - - - - - - - - diff --git a/WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml.cs b/WheelWizard/Views/Patterns/ModBrowserListItem.axaml.cs similarity index 98% rename from WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml.cs rename to WheelWizard/Views/Patterns/ModBrowserListItem.axaml.cs index 199eae95..22cc887e 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/ModBrowserListItem.axaml.cs +++ b/WheelWizard/Views/Patterns/ModBrowserListItem.axaml.cs @@ -3,7 +3,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Media.Imaging; -namespace WheelWizard.Views.Components; +namespace WheelWizard.Views.Patterns; public class ModBrowserListItem : TemplatedControl { diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml b/WheelWizard/Views/Patterns/PlayerListItem.axaml similarity index 91% rename from WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml rename to WheelWizard/Views/Patterns/PlayerListItem.axaml index c463717a..bc58f7e6 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml +++ b/WheelWizard/Views/Patterns/PlayerListItem.axaml @@ -2,11 +2,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:lang="clr-namespace:WheelWizard.Resources.Languages" xmlns:components="using:WheelWizard.Views.Components" + xmlns:patterns="clr-namespace:WheelWizard.Views.Patterns" xmlns:miiVars="using:WheelWizard.MiiImages.Domain" - xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.BehaviorComponent"> + xmlns:behaviorComponent="clr-namespace:WheelWizard.Views.Patterns"> - - - - - - - - - diff --git a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs b/WheelWizard/Views/Patterns/PlayerListItem.axaml.cs similarity index 95% rename from WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs rename to WheelWizard/Views/Patterns/PlayerListItem.axaml.cs index 8b26cc67..9a8e2db5 100644 --- a/WheelWizard/Views/Components/WhWzLibrary/PlayerListItem.axaml.cs +++ b/WheelWizard/Views/Patterns/PlayerListItem.axaml.cs @@ -4,12 +4,13 @@ using Avalonia.Interactivity; using WheelWizard.WheelWizardData; using WheelWizard.WiiManagement.MiiManagement.Domain.Mii; +using Badge = WheelWizard.Views.Components.Badge; -namespace WheelWizard.Views.Components; +namespace WheelWizard.Views.Patterns; public class PlayerListItem : TemplatedControl { - private Button? _joinRoomButton; + private Avalonia.Controls.Button? _joinRoomButton; public static readonly StyledProperty IsOnlineProperty = AvaloniaProperty.Register(nameof(IsOnline)); public static readonly StyledProperty ShowJoinRoomButtonProperty = AvaloniaProperty.Register( @@ -147,7 +148,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) if (_joinRoomButton != null) _joinRoomButton.Click -= JoinRoom_OnClick; - _joinRoomButton = e.NameScope.Find