Initial pass
This commit is contained in:
8
MarketAlly.MASpotlightTour.slnx
Normal file
8
MarketAlly.MASpotlightTour.slnx
Normal file
@@ -0,0 +1,8 @@
|
||||
<Solution>
|
||||
<Project Path="MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj">
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="Test.SpotlightTour/Test.SpotlightTour.csproj" Id="ab7d572f-0431-45f1-95ed-1096d93b2ce0">
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Solution>
|
||||
161
MarketAlly.MASpotlightTour/ArrowIndicator.cs
Normal file
161
MarketAlly.MASpotlightTour/ArrowIndicator.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Microsoft.Maui.Controls.Shapes;
|
||||
using Path = Microsoft.Maui.Controls.Shapes.Path;
|
||||
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// An arrow indicator that points from the callout card toward the spotlight target.
|
||||
/// </summary>
|
||||
public class ArrowIndicator : ContentView
|
||||
{
|
||||
private readonly Path _arrowPath;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
public static readonly BindableProperty ArrowColorProperty =
|
||||
BindableProperty.Create(nameof(ArrowColor), typeof(Color), typeof(ArrowIndicator), Colors.White,
|
||||
propertyChanged: (b, _, n) => ((ArrowIndicator)b).UpdateArrowColor((Color)n));
|
||||
|
||||
public Color ArrowColor
|
||||
{
|
||||
get => (Color)GetValue(ArrowColorProperty);
|
||||
set => SetValue(ArrowColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ArrowSizeProperty =
|
||||
BindableProperty.Create(nameof(ArrowSize), typeof(double), typeof(ArrowIndicator), 16.0);
|
||||
|
||||
public double ArrowSize
|
||||
{
|
||||
get => (double)GetValue(ArrowSizeProperty);
|
||||
set => SetValue(ArrowSizeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DirectionProperty =
|
||||
BindableProperty.Create(nameof(Direction), typeof(CalloutPlacement), typeof(ArrowIndicator),
|
||||
CalloutPlacement.Bottom,
|
||||
propertyChanged: (b, _, _) => ((ArrowIndicator)b).UpdateArrow());
|
||||
|
||||
/// <summary>
|
||||
/// The direction the arrow points (toward the target).
|
||||
/// </summary>
|
||||
public CalloutPlacement Direction
|
||||
{
|
||||
get => (CalloutPlacement)GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ArrowIndicator()
|
||||
{
|
||||
_arrowPath = new Path
|
||||
{
|
||||
Fill = new SolidColorBrush(ArrowColor),
|
||||
Stroke = null
|
||||
};
|
||||
|
||||
Content = _arrowPath;
|
||||
UpdateArrow();
|
||||
}
|
||||
|
||||
private void UpdateArrowColor(Color color)
|
||||
{
|
||||
_arrowPath.Fill = new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
private void UpdateArrow()
|
||||
{
|
||||
var size = ArrowSize;
|
||||
var geometry = new PathGeometry();
|
||||
var figure = new PathFigure { IsClosed = true };
|
||||
|
||||
switch (Direction)
|
||||
{
|
||||
case CalloutPlacement.Top:
|
||||
// Arrow pointing up (callout is below target)
|
||||
figure.StartPoint = new Point(size / 2, 0);
|
||||
figure.Segments.Add(new LineSegment(new Point(size, size)));
|
||||
figure.Segments.Add(new LineSegment(new Point(0, size)));
|
||||
WidthRequest = size;
|
||||
HeightRequest = size;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Bottom:
|
||||
// Arrow pointing down (callout is above target)
|
||||
figure.StartPoint = new Point(0, 0);
|
||||
figure.Segments.Add(new LineSegment(new Point(size, 0)));
|
||||
figure.Segments.Add(new LineSegment(new Point(size / 2, size)));
|
||||
WidthRequest = size;
|
||||
HeightRequest = size;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Left:
|
||||
// Arrow pointing left (callout is to the right of target)
|
||||
figure.StartPoint = new Point(0, size / 2);
|
||||
figure.Segments.Add(new LineSegment(new Point(size, 0)));
|
||||
figure.Segments.Add(new LineSegment(new Point(size, size)));
|
||||
WidthRequest = size;
|
||||
HeightRequest = size;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Right:
|
||||
// Arrow pointing right (callout is to the left of target)
|
||||
figure.StartPoint = new Point(size, size / 2);
|
||||
figure.Segments.Add(new LineSegment(new Point(0, 0)));
|
||||
figure.Segments.Add(new LineSegment(new Point(0, size)));
|
||||
WidthRequest = size;
|
||||
HeightRequest = size;
|
||||
break;
|
||||
|
||||
default:
|
||||
// No arrow for Auto
|
||||
IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
geometry.Figures.Add(figure);
|
||||
_arrowPath.Data = geometry;
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the arrow between the callout and spotlight.
|
||||
/// </summary>
|
||||
public void PositionBetween(Rect calloutBounds, Rect spotlightBounds, CalloutPlacement placement)
|
||||
{
|
||||
Direction = placement;
|
||||
var size = ArrowSize;
|
||||
|
||||
switch (placement)
|
||||
{
|
||||
case CalloutPlacement.Top:
|
||||
// Callout is above, arrow points up from bottom of callout toward target
|
||||
TranslationX = calloutBounds.Center.X - size / 2;
|
||||
TranslationY = calloutBounds.Bottom;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Bottom:
|
||||
// Callout is below, arrow points down from top of callout toward target
|
||||
TranslationX = calloutBounds.Center.X - size / 2;
|
||||
TranslationY = calloutBounds.Top - size;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Left:
|
||||
// Callout is to the left, arrow points left from right of callout toward target
|
||||
TranslationX = calloutBounds.Right;
|
||||
TranslationY = calloutBounds.Center.Y - size / 2;
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Right:
|
||||
// Callout is to the right, arrow points right from left of callout toward target
|
||||
TranslationX = calloutBounds.Left - size;
|
||||
TranslationY = calloutBounds.Center.Y - size / 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
IsVisible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
460
MarketAlly.MASpotlightTour/CalloutCard.cs
Normal file
460
MarketAlly.MASpotlightTour/CalloutCard.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using Microsoft.Maui.Controls.Shapes;
|
||||
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// A card that displays the title, description, and navigation buttons for an onboarding step.
|
||||
/// </summary>
|
||||
public class CalloutCard : Border
|
||||
{
|
||||
private readonly Label _titleLabel;
|
||||
private readonly Label _descriptionLabel;
|
||||
private readonly Button _previousButton;
|
||||
private readonly Button _nextButton;
|
||||
private readonly Button _closeButton;
|
||||
private readonly Grid _buttonContainer;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
public static readonly BindableProperty TitleProperty =
|
||||
BindableProperty.Create(nameof(Title), typeof(string), typeof(CalloutCard), null,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._titleLabel.Text = (string?)n);
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => (string?)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DescriptionProperty =
|
||||
BindableProperty.Create(nameof(Description), typeof(string), typeof(CalloutCard), null,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._descriptionLabel.Text = (string?)n);
|
||||
|
||||
public string? Description
|
||||
{
|
||||
get => (string?)GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowPreviousButtonProperty =
|
||||
BindableProperty.Create(nameof(ShowPreviousButton), typeof(bool), typeof(CalloutCard), true,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._previousButton.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowPreviousButton
|
||||
{
|
||||
get => (bool)GetValue(ShowPreviousButtonProperty);
|
||||
set => SetValue(ShowPreviousButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowNextButtonProperty =
|
||||
BindableProperty.Create(nameof(ShowNextButton), typeof(bool), typeof(CalloutCard), true,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._nextButton.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowNextButton
|
||||
{
|
||||
get => (bool)GetValue(ShowNextButtonProperty);
|
||||
set => SetValue(ShowNextButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowCloseButtonProperty =
|
||||
BindableProperty.Create(nameof(ShowCloseButton), typeof(bool), typeof(CalloutCard), true,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._closeButton.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => (bool)GetValue(ShowCloseButtonProperty);
|
||||
set => SetValue(ShowCloseButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty PreviousButtonTextProperty =
|
||||
BindableProperty.Create(nameof(PreviousButtonText), typeof(string), typeof(CalloutCard), "Previous",
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._previousButton.Text = (string?)n);
|
||||
|
||||
public string? PreviousButtonText
|
||||
{
|
||||
get => (string?)GetValue(PreviousButtonTextProperty);
|
||||
set => SetValue(PreviousButtonTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty NextButtonTextProperty =
|
||||
BindableProperty.Create(nameof(NextButtonText), typeof(string), typeof(CalloutCard), "Next",
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._nextButton.Text = (string?)n);
|
||||
|
||||
public string? NextButtonText
|
||||
{
|
||||
get => (string?)GetValue(NextButtonTextProperty);
|
||||
set => SetValue(NextButtonTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty CloseButtonTextProperty =
|
||||
BindableProperty.Create(nameof(CloseButtonText), typeof(string), typeof(CalloutCard), "Done",
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._closeButton.Text = (string?)n);
|
||||
|
||||
public string? CloseButtonText
|
||||
{
|
||||
get => (string?)GetValue(CloseButtonTextProperty);
|
||||
set => SetValue(CloseButtonTextProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty CardBackgroundColorProperty =
|
||||
BindableProperty.Create(nameof(CardBackgroundColor), typeof(Color), typeof(CalloutCard), Colors.White,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b).BackgroundColor = (Color)n);
|
||||
|
||||
public Color CardBackgroundColor
|
||||
{
|
||||
get => (Color)GetValue(CardBackgroundColorProperty);
|
||||
set => SetValue(CardBackgroundColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowNavigationButtonsProperty =
|
||||
BindableProperty.Create(nameof(ShowNavigationButtons), typeof(bool), typeof(CalloutCard), true,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._buttonContainer.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowNavigationButtons
|
||||
{
|
||||
get => (bool)GetValue(ShowNavigationButtonsProperty);
|
||||
set => SetValue(ShowNavigationButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty TitleColorProperty =
|
||||
BindableProperty.Create(nameof(TitleColor), typeof(Color), typeof(CalloutCard), Colors.Black,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._titleLabel.TextColor = (Color)n);
|
||||
|
||||
public Color TitleColor
|
||||
{
|
||||
get => (Color)GetValue(TitleColorProperty);
|
||||
set => SetValue(TitleColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DescriptionColorProperty =
|
||||
BindableProperty.Create(nameof(DescriptionColor), typeof(Color), typeof(CalloutCard), Colors.DarkGray,
|
||||
propertyChanged: (b, _, n) => ((CalloutCard)b)._descriptionLabel.TextColor = (Color)n);
|
||||
|
||||
public Color DescriptionColor
|
||||
{
|
||||
get => (Color)GetValue(DescriptionColorProperty);
|
||||
set => SetValue(DescriptionColorProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler? PreviousClicked;
|
||||
public event EventHandler? NextClicked;
|
||||
public event EventHandler? CloseClicked;
|
||||
|
||||
#endregion
|
||||
|
||||
public CalloutCard()
|
||||
{
|
||||
BackgroundColor = Colors.White;
|
||||
StrokeShape = new RoundRectangle { CornerRadius = 12 };
|
||||
Stroke = Colors.Transparent;
|
||||
Padding = new Thickness(16);
|
||||
|
||||
// Add shadow
|
||||
Shadow = new Shadow
|
||||
{
|
||||
Brush = new SolidColorBrush(Colors.Black),
|
||||
Offset = new Point(0, 4),
|
||||
Radius = 8,
|
||||
Opacity = 0.3f
|
||||
};
|
||||
|
||||
_titleLabel = new Label
|
||||
{
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
FontSize = 18,
|
||||
TextColor = Colors.Black,
|
||||
LineBreakMode = LineBreakMode.WordWrap
|
||||
};
|
||||
|
||||
_descriptionLabel = new Label
|
||||
{
|
||||
FontSize = 14,
|
||||
TextColor = Colors.DarkGray,
|
||||
LineBreakMode = LineBreakMode.WordWrap
|
||||
};
|
||||
|
||||
_previousButton = new Button
|
||||
{
|
||||
Text = "Previous",
|
||||
BackgroundColor = Colors.Transparent,
|
||||
TextColor = Colors.Gray,
|
||||
BorderWidth = 0,
|
||||
Padding = new Thickness(8, 4)
|
||||
};
|
||||
_previousButton.Clicked += (_, _) => PreviousClicked?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
_nextButton = new Button
|
||||
{
|
||||
Text = "Next",
|
||||
BackgroundColor = Color.FromArgb("#007AFF"),
|
||||
TextColor = Colors.White,
|
||||
CornerRadius = 8,
|
||||
Padding = new Thickness(16, 8)
|
||||
};
|
||||
_nextButton.Clicked += (_, _) => NextClicked?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
_closeButton = new Button
|
||||
{
|
||||
Text = "Done",
|
||||
BackgroundColor = Color.FromArgb("#34C759"),
|
||||
TextColor = Colors.White,
|
||||
CornerRadius = 8,
|
||||
Padding = new Thickness(16, 8),
|
||||
IsVisible = false
|
||||
};
|
||||
_closeButton.Clicked += (_, _) => CloseClicked?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
_buttonContainer = new Grid
|
||||
{
|
||||
ColumnDefinitions =
|
||||
{
|
||||
new ColumnDefinition(GridLength.Star),
|
||||
new ColumnDefinition(GridLength.Auto),
|
||||
new ColumnDefinition(GridLength.Auto)
|
||||
},
|
||||
ColumnSpacing = 8,
|
||||
Margin = new Thickness(0, 12, 0, 0)
|
||||
};
|
||||
|
||||
_buttonContainer.Add(_previousButton, 0);
|
||||
_buttonContainer.Add(_nextButton, 1);
|
||||
_buttonContainer.Add(_closeButton, 2);
|
||||
|
||||
Content = new VerticalStackLayout
|
||||
{
|
||||
Spacing = 8,
|
||||
Children =
|
||||
{
|
||||
_titleLabel,
|
||||
_descriptionLabel,
|
||||
_buttonContainer
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the card content and button visibility based on step position.
|
||||
/// </summary>
|
||||
public void UpdateForStep(string? title, string? description, bool isFirst, bool isLast)
|
||||
{
|
||||
Title = title;
|
||||
Description = description;
|
||||
ShowPreviousButton = !isFirst;
|
||||
ShowNextButton = !isLast;
|
||||
_closeButton.IsVisible = isLast || ShowCloseButton;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the card relative to a spotlight rectangle.
|
||||
/// </summary>
|
||||
public void PositionRelativeToSpotlight(
|
||||
Rect spotlightRect,
|
||||
Size containerSize,
|
||||
CalloutPlacement placement,
|
||||
double margin = 12)
|
||||
{
|
||||
var cardWidth = Math.Min(containerSize.Width * 0.85, 400);
|
||||
WidthRequest = cardWidth;
|
||||
|
||||
// Measure to get height
|
||||
var measured = Measure(cardWidth, double.PositiveInfinity);
|
||||
var cardHeight = measured.Height;
|
||||
|
||||
double x, y;
|
||||
|
||||
var effectivePlacement = placement == CalloutPlacement.Auto
|
||||
? DetermineAutoPlacement(spotlightRect, containerSize, cardHeight, margin)
|
||||
: placement;
|
||||
|
||||
switch (effectivePlacement)
|
||||
{
|
||||
case CalloutPlacement.Top:
|
||||
x = Math.Max(margin, Math.Min((spotlightRect.Center.X - cardWidth / 2), containerSize.Width - cardWidth - margin));
|
||||
y = Math.Max(margin, spotlightRect.Top - cardHeight - margin);
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Bottom:
|
||||
x = Math.Max(margin, Math.Min((spotlightRect.Center.X - cardWidth / 2), containerSize.Width - cardWidth - margin));
|
||||
y = Math.Min(containerSize.Height - cardHeight - margin, spotlightRect.Bottom + margin);
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Left:
|
||||
x = Math.Max(margin, spotlightRect.Left - cardWidth - margin);
|
||||
y = ClampVertical(spotlightRect.Center.Y - cardHeight / 2, cardHeight, containerSize.Height, margin);
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Right:
|
||||
x = Math.Min(containerSize.Width - cardWidth - margin, spotlightRect.Right + margin);
|
||||
y = ClampVertical(spotlightRect.Center.Y - cardHeight / 2, cardHeight, containerSize.Height, margin);
|
||||
break;
|
||||
|
||||
default:
|
||||
x = (containerSize.Width - cardWidth) / 2;
|
||||
y = containerSize.Height / 2 - cardHeight / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
TranslationX = x;
|
||||
TranslationY = y;
|
||||
}
|
||||
|
||||
private static CalloutPlacement DetermineAutoPlacement(Rect spotlightRect, Size containerSize, double cardHeight, double margin)
|
||||
{
|
||||
var spaceBelow = containerSize.Height - spotlightRect.Bottom - margin;
|
||||
var spaceAbove = spotlightRect.Top - margin;
|
||||
var spaceRight = containerSize.Width - spotlightRect.Right - margin;
|
||||
var spaceLeft = spotlightRect.Left - margin;
|
||||
|
||||
// Prefer bottom, then top, then right, then left
|
||||
if (spaceBelow >= cardHeight + margin)
|
||||
return CalloutPlacement.Bottom;
|
||||
if (spaceAbove >= cardHeight + margin)
|
||||
return CalloutPlacement.Top;
|
||||
if (spaceRight >= 200)
|
||||
return CalloutPlacement.Right;
|
||||
if (spaceLeft >= 200)
|
||||
return CalloutPlacement.Left;
|
||||
|
||||
// Default to bottom if nothing fits well
|
||||
return CalloutPlacement.Bottom;
|
||||
}
|
||||
|
||||
private static double ClampVertical(double y, double height, double containerHeight, double margin)
|
||||
{
|
||||
if (y < margin)
|
||||
return margin;
|
||||
if (y + height + margin > containerHeight)
|
||||
return containerHeight - height - margin;
|
||||
return y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates the card to a new position.
|
||||
/// </summary>
|
||||
public async Task AnimateToPositionAsync(double x, double y, uint duration = 250)
|
||||
{
|
||||
await this.TranslateTo(x, y, duration, Easing.CubicInOut);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the card in a specific screen corner.
|
||||
/// </summary>
|
||||
public void PositionInCorner(CalloutCorner corner, Size containerSize, double margin = 16)
|
||||
{
|
||||
var cardWidth = Math.Min(containerSize.Width * 0.85, 400);
|
||||
WidthRequest = cardWidth;
|
||||
|
||||
var measured = Measure(cardWidth, double.PositiveInfinity);
|
||||
var cardHeight = measured.Height;
|
||||
|
||||
double x, y;
|
||||
|
||||
switch (corner)
|
||||
{
|
||||
case CalloutCorner.TopLeft:
|
||||
x = margin;
|
||||
y = margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.TopRight:
|
||||
x = containerSize.Width - cardWidth - margin;
|
||||
y = margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.BottomLeft:
|
||||
x = margin;
|
||||
y = containerSize.Height - cardHeight - margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.BottomRight:
|
||||
default:
|
||||
x = containerSize.Width - cardWidth - margin;
|
||||
y = containerSize.Height - cardHeight - margin;
|
||||
break;
|
||||
}
|
||||
|
||||
TranslationX = x;
|
||||
TranslationY = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the best corner that doesn't overlap with the spotlight area.
|
||||
/// </summary>
|
||||
public static CalloutCorner DetermineBestCorner(Rect spotlightRect, Size containerSize, double cardWidth, double cardHeight, double margin = 16)
|
||||
{
|
||||
// Calculate the effective card size for collision checking
|
||||
var actualCardWidth = Math.Min(containerSize.Width * 0.85, cardWidth);
|
||||
|
||||
// Check each corner and calculate overlap with spotlight
|
||||
var corners = new[]
|
||||
{
|
||||
CalloutCorner.TopLeft,
|
||||
CalloutCorner.TopRight,
|
||||
CalloutCorner.BottomLeft,
|
||||
CalloutCorner.BottomRight
|
||||
};
|
||||
|
||||
CalloutCorner bestCorner = CalloutCorner.BottomLeft;
|
||||
double minOverlap = double.MaxValue;
|
||||
|
||||
foreach (var corner in corners)
|
||||
{
|
||||
var cardRect = GetCornerRect(corner, containerSize, actualCardWidth, cardHeight, margin);
|
||||
var overlap = CalculateOverlap(cardRect, spotlightRect);
|
||||
|
||||
if (overlap < minOverlap)
|
||||
{
|
||||
minOverlap = overlap;
|
||||
bestCorner = corner;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCorner;
|
||||
}
|
||||
|
||||
private static Rect GetCornerRect(CalloutCorner corner, Size containerSize, double cardWidth, double cardHeight, double margin)
|
||||
{
|
||||
return corner switch
|
||||
{
|
||||
CalloutCorner.TopLeft => new Rect(margin, margin, cardWidth, cardHeight),
|
||||
CalloutCorner.TopRight => new Rect(containerSize.Width - cardWidth - margin, margin, cardWidth, cardHeight),
|
||||
CalloutCorner.BottomLeft => new Rect(margin, containerSize.Height - cardHeight - margin, cardWidth, cardHeight),
|
||||
CalloutCorner.BottomRight => new Rect(containerSize.Width - cardWidth - margin, containerSize.Height - cardHeight - margin, cardWidth, cardHeight),
|
||||
_ => new Rect(margin, containerSize.Height - cardHeight - margin, cardWidth, cardHeight)
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateOverlap(Rect rect1, Rect rect2)
|
||||
{
|
||||
var xOverlap = Math.Max(0, Math.Min(rect1.Right, rect2.Right) - Math.Max(rect1.Left, rect2.Left));
|
||||
var yOverlap = Math.Max(0, Math.Min(rect1.Bottom, rect2.Bottom) - Math.Max(rect1.Top, rect2.Top));
|
||||
return xOverlap * yOverlap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies light or dark theme to the callout card.
|
||||
/// </summary>
|
||||
public void ApplyTheme(bool isDark)
|
||||
{
|
||||
if (isDark)
|
||||
{
|
||||
CardBackgroundColor = Color.FromArgb("#1C1C1E");
|
||||
TitleColor = Colors.White;
|
||||
DescriptionColor = Color.FromArgb("#ABABAB");
|
||||
Stroke = Color.FromArgb("#3A3A3C");
|
||||
_previousButton.TextColor = Color.FromArgb("#8E8E93");
|
||||
}
|
||||
else
|
||||
{
|
||||
CardBackgroundColor = Colors.White;
|
||||
TitleColor = Colors.Black;
|
||||
DescriptionColor = Colors.DarkGray;
|
||||
Stroke = Colors.Transparent;
|
||||
_previousButton.TextColor = Colors.Gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
311
MarketAlly.MASpotlightTour/CornerNavigator.cs
Normal file
311
MarketAlly.MASpotlightTour/CornerNavigator.cs
Normal file
@@ -0,0 +1,311 @@
|
||||
using Microsoft.Maui.Controls.Shapes;
|
||||
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// A corner-positioned navigator with step indicator and quick nav buttons.
|
||||
/// </summary>
|
||||
public class CornerNavigator : Border
|
||||
{
|
||||
private readonly Label _stepIndicator;
|
||||
private readonly Border _previousButton;
|
||||
private readonly Border _nextButton;
|
||||
private readonly Border _skipButton;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
public static readonly BindableProperty CurrentStepProperty =
|
||||
BindableProperty.Create(nameof(CurrentStep), typeof(int), typeof(CornerNavigator), 0,
|
||||
propertyChanged: (b, _, _) => ((CornerNavigator)b).UpdateStepIndicator());
|
||||
|
||||
public int CurrentStep
|
||||
{
|
||||
get => (int)GetValue(CurrentStepProperty);
|
||||
set => SetValue(CurrentStepProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty TotalStepsProperty =
|
||||
BindableProperty.Create(nameof(TotalSteps), typeof(int), typeof(CornerNavigator), 0,
|
||||
propertyChanged: (b, _, _) => ((CornerNavigator)b).UpdateStepIndicator());
|
||||
|
||||
public int TotalSteps
|
||||
{
|
||||
get => (int)GetValue(TotalStepsProperty);
|
||||
set => SetValue(TotalStepsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty PlacementProperty =
|
||||
BindableProperty.Create(nameof(Placement), typeof(CornerNavigatorPlacement), typeof(CornerNavigator),
|
||||
CornerNavigatorPlacement.BottomRight,
|
||||
propertyChanged: (b, _, _) => ((CornerNavigator)b).UpdatePlacement());
|
||||
|
||||
public CornerNavigatorPlacement Placement
|
||||
{
|
||||
get => (CornerNavigatorPlacement)GetValue(PlacementProperty);
|
||||
set => SetValue(PlacementProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowSkipButtonProperty =
|
||||
BindableProperty.Create(nameof(ShowSkipButton), typeof(bool), typeof(CornerNavigator), true,
|
||||
propertyChanged: (b, _, n) => ((CornerNavigator)b)._skipButton.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowSkipButton
|
||||
{
|
||||
get => (bool)GetValue(ShowSkipButtonProperty);
|
||||
set => SetValue(ShowSkipButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowNavigationButtonsProperty =
|
||||
BindableProperty.Create(nameof(ShowNavigationButtons), typeof(bool), typeof(CornerNavigator), true,
|
||||
propertyChanged: (b, _, n) => ((CornerNavigator)b).UpdateNavigationButtonsVisibility((bool)n));
|
||||
|
||||
public bool ShowNavigationButtons
|
||||
{
|
||||
get => (bool)GetValue(ShowNavigationButtonsProperty);
|
||||
set => SetValue(ShowNavigationButtonsProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty MarginFromEdgeProperty =
|
||||
BindableProperty.Create(nameof(MarginFromEdge), typeof(double), typeof(CornerNavigator), 16.0,
|
||||
propertyChanged: (b, _, _) => ((CornerNavigator)b).UpdatePlacement());
|
||||
|
||||
public double MarginFromEdge
|
||||
{
|
||||
get => (double)GetValue(MarginFromEdgeProperty);
|
||||
set => SetValue(MarginFromEdgeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty NavigatorBackgroundColorProperty =
|
||||
BindableProperty.Create(nameof(NavigatorBackgroundColor), typeof(Color), typeof(CornerNavigator),
|
||||
Color.FromArgb("#1C1C1E"),
|
||||
propertyChanged: (b, _, n) => ((CornerNavigator)b).BackgroundColor = (Color)n);
|
||||
|
||||
public Color NavigatorBackgroundColor
|
||||
{
|
||||
get => (Color)GetValue(NavigatorBackgroundColorProperty);
|
||||
set => SetValue(NavigatorBackgroundColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty SkipButtonTextProperty =
|
||||
BindableProperty.Create(nameof(SkipButtonText), typeof(string), typeof(CornerNavigator), "Done",
|
||||
propertyChanged: (b, _, n) => ((CornerNavigator)b).UpdateSkipButtonText((string?)n));
|
||||
|
||||
public string? SkipButtonText
|
||||
{
|
||||
get => (string?)GetValue(SkipButtonTextProperty);
|
||||
set => SetValue(SkipButtonTextProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler? PreviousClicked;
|
||||
public event EventHandler? NextClicked;
|
||||
public event EventHandler? SkipClicked;
|
||||
|
||||
#endregion
|
||||
|
||||
public CornerNavigator()
|
||||
{
|
||||
BackgroundColor = Color.FromArgb("#1C1C1E");
|
||||
StrokeShape = new RoundRectangle { CornerRadius = 25 };
|
||||
Stroke = Color.FromArgb("#3A3A3C");
|
||||
StrokeThickness = 1;
|
||||
Padding = new Thickness(6);
|
||||
HorizontalOptions = LayoutOptions.Start;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
|
||||
Shadow = new Shadow
|
||||
{
|
||||
Brush = new SolidColorBrush(Colors.Black),
|
||||
Offset = new Point(0, 4),
|
||||
Radius = 12,
|
||||
Opacity = 0.4f
|
||||
};
|
||||
|
||||
// Previous button - circular with arrow
|
||||
_previousButton = CreateNavButton("\u276E", Color.FromArgb("#3A3A3C")); // ❮
|
||||
var prevTap = new TapGestureRecognizer();
|
||||
prevTap.Tapped += (_, _) => { if (_previousButton.Opacity > 0.5) PreviousClicked?.Invoke(this, EventArgs.Empty); };
|
||||
_previousButton.GestureRecognizers.Add(prevTap);
|
||||
|
||||
// Step indicator
|
||||
_stepIndicator = new Label
|
||||
{
|
||||
TextColor = Colors.White,
|
||||
FontSize = 14,
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
Margin = new Thickness(12, 0)
|
||||
};
|
||||
|
||||
// Next button - circular with arrow, highlighted
|
||||
_nextButton = CreateNavButton("\u276F", Color.FromArgb("#007AFF")); // ❯
|
||||
var nextTap = new TapGestureRecognizer();
|
||||
nextTap.Tapped += (_, _) => { if (_nextButton.Opacity > 0.5) NextClicked?.Invoke(this, EventArgs.Empty); };
|
||||
_nextButton.GestureRecognizers.Add(nextTap);
|
||||
|
||||
// Done button - pill shaped
|
||||
_skipButton = new Border
|
||||
{
|
||||
BackgroundColor = Color.FromArgb("#FF3B30"),
|
||||
StrokeShape = new RoundRectangle { CornerRadius = 14 },
|
||||
Stroke = Colors.Transparent,
|
||||
Padding = new Thickness(14, 6),
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
Content = new Label
|
||||
{
|
||||
Text = "Done",
|
||||
TextColor = Colors.White,
|
||||
FontSize = 13,
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.Center
|
||||
}
|
||||
};
|
||||
var skipTap = new TapGestureRecognizer();
|
||||
skipTap.Tapped += (_, _) => SkipClicked?.Invoke(this, EventArgs.Empty);
|
||||
_skipButton.GestureRecognizers.Add(skipTap);
|
||||
|
||||
var layout = new HorizontalStackLayout
|
||||
{
|
||||
Spacing = 4,
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
Children =
|
||||
{
|
||||
_previousButton,
|
||||
_stepIndicator,
|
||||
_nextButton,
|
||||
_skipButton
|
||||
}
|
||||
};
|
||||
|
||||
Content = layout;
|
||||
UpdateStepIndicator();
|
||||
UpdatePlacement();
|
||||
}
|
||||
|
||||
private Border CreateNavButton(string symbol, Color bgColor)
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
BackgroundColor = bgColor,
|
||||
StrokeShape = new RoundRectangle { CornerRadius = 18 },
|
||||
Stroke = Colors.Transparent,
|
||||
WidthRequest = 36,
|
||||
HeightRequest = 36,
|
||||
Content = new Label
|
||||
{
|
||||
Text = symbol,
|
||||
TextColor = Colors.White,
|
||||
FontSize = 16,
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
VerticalOptions = LayoutOptions.Center
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateSkipButtonText(string? text)
|
||||
{
|
||||
if (_skipButton.Content is Label label)
|
||||
label.Text = text ?? "Done";
|
||||
}
|
||||
|
||||
private void UpdateStepIndicator()
|
||||
{
|
||||
_stepIndicator.Text = $"{CurrentStep + 1} / {TotalSteps}";
|
||||
|
||||
// Update button states
|
||||
var canGoPrev = CurrentStep > 0;
|
||||
_previousButton.Opacity = canGoPrev ? 1.0 : 0.3;
|
||||
|
||||
var canGoNext = CurrentStep < TotalSteps - 1;
|
||||
_nextButton.Opacity = canGoNext ? 1.0 : 0.3;
|
||||
}
|
||||
|
||||
private void UpdateNavigationButtonsVisibility(bool show)
|
||||
{
|
||||
_previousButton.IsVisible = show;
|
||||
_nextButton.IsVisible = show;
|
||||
}
|
||||
|
||||
private void UpdatePlacement()
|
||||
{
|
||||
switch (Placement)
|
||||
{
|
||||
case CornerNavigatorPlacement.TopLeft:
|
||||
HorizontalOptions = LayoutOptions.Start;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
Margin = new Thickness(MarginFromEdge, MarginFromEdge, 0, 0);
|
||||
break;
|
||||
|
||||
case CornerNavigatorPlacement.TopRight:
|
||||
HorizontalOptions = LayoutOptions.End;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
Margin = new Thickness(0, MarginFromEdge, MarginFromEdge, 0);
|
||||
break;
|
||||
|
||||
case CornerNavigatorPlacement.BottomLeft:
|
||||
HorizontalOptions = LayoutOptions.Start;
|
||||
VerticalOptions = LayoutOptions.End;
|
||||
Margin = new Thickness(MarginFromEdge, 0, 0, MarginFromEdge);
|
||||
break;
|
||||
|
||||
case CornerNavigatorPlacement.BottomRight:
|
||||
default:
|
||||
HorizontalOptions = LayoutOptions.End;
|
||||
VerticalOptions = LayoutOptions.End;
|
||||
Margin = new Thickness(0, 0, MarginFromEdge, MarginFromEdge);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the navigator for a specific step.
|
||||
/// </summary>
|
||||
public void UpdateForStep(int currentIndex, int totalCount)
|
||||
{
|
||||
CurrentStep = currentIndex;
|
||||
TotalSteps = totalCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies light or dark theme to the corner navigator.
|
||||
/// </summary>
|
||||
public void ApplyTheme(bool isDark)
|
||||
{
|
||||
if (isDark)
|
||||
{
|
||||
NavigatorBackgroundColor = Color.FromArgb("#1C1C1E");
|
||||
Stroke = Color.FromArgb("#3A3A3C");
|
||||
_stepIndicator.TextColor = Colors.White;
|
||||
_previousButton.BackgroundColor = Color.FromArgb("#3A3A3C");
|
||||
if (_previousButton.Content is Label prevLabel)
|
||||
prevLabel.TextColor = Colors.White;
|
||||
_nextButton.BackgroundColor = Color.FromArgb("#0A84FF");
|
||||
if (_nextButton.Content is Label nextLabel)
|
||||
nextLabel.TextColor = Colors.White;
|
||||
_skipButton.BackgroundColor = Color.FromArgb("#FF453A");
|
||||
if (_skipButton.Content is Label skipLabel)
|
||||
skipLabel.TextColor = Colors.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigatorBackgroundColor = Colors.White;
|
||||
Stroke = Color.FromArgb("#E5E5EA");
|
||||
_stepIndicator.TextColor = Colors.Black;
|
||||
_previousButton.BackgroundColor = Color.FromArgb("#E5E5EA");
|
||||
if (_previousButton.Content is Label prevLabel)
|
||||
prevLabel.TextColor = Color.FromArgb("#3A3A3C");
|
||||
_nextButton.BackgroundColor = Color.FromArgb("#007AFF");
|
||||
if (_nextButton.Content is Label nextLabel)
|
||||
nextLabel.TextColor = Colors.White;
|
||||
_skipButton.BackgroundColor = Color.FromArgb("#FF3B30");
|
||||
if (_skipButton.Content is Label skipLabel)
|
||||
skipLabel.TextColor = Colors.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
200
MarketAlly.MASpotlightTour/Enums.cs
Normal file
200
MarketAlly.MASpotlightTour/Enums.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the visual theme for the tour components.
|
||||
/// </summary>
|
||||
public enum TourTheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Light theme with white backgrounds and dark text.
|
||||
/// </summary>
|
||||
Light,
|
||||
|
||||
/// <summary>
|
||||
/// Dark theme with dark backgrounds and light text.
|
||||
/// </summary>
|
||||
Dark,
|
||||
|
||||
/// <summary>
|
||||
/// Automatically follows the system theme.
|
||||
/// </summary>
|
||||
System
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies where the callout card is positioned relative to the spotlight.
|
||||
/// </summary>
|
||||
public enum CalloutPlacement
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically determine placement based on available space.
|
||||
/// </summary>
|
||||
Auto,
|
||||
|
||||
/// <summary>
|
||||
/// Position callout above the spotlight.
|
||||
/// </summary>
|
||||
Top,
|
||||
|
||||
/// <summary>
|
||||
/// Position callout below the spotlight.
|
||||
/// </summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// Position callout to the left of the spotlight.
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// Position callout to the right of the spotlight.
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the shape of the spotlight cutout.
|
||||
/// </summary>
|
||||
public enum SpotlightShape
|
||||
{
|
||||
/// <summary>
|
||||
/// Rectangular cutout matching the element bounds.
|
||||
/// </summary>
|
||||
Rectangle,
|
||||
|
||||
/// <summary>
|
||||
/// Rounded rectangle cutout with configurable corner radius.
|
||||
/// </summary>
|
||||
RoundedRectangle,
|
||||
|
||||
/// <summary>
|
||||
/// Circular cutout centered on the element.
|
||||
/// </summary>
|
||||
Circle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies behavior when the user taps on the spotlighted element.
|
||||
/// </summary>
|
||||
public enum SpotlightTapBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Tapping the spotlight does nothing; only nav buttons work.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Tapping the spotlight advances to the next step.
|
||||
/// </summary>
|
||||
Advance,
|
||||
|
||||
/// <summary>
|
||||
/// Tapping the spotlight closes the tour.
|
||||
/// </summary>
|
||||
Close,
|
||||
|
||||
/// <summary>
|
||||
/// Allow interaction with the underlying element (pass-through).
|
||||
/// </summary>
|
||||
AllowInteraction
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the corner position for the navigator control.
|
||||
/// </summary>
|
||||
public enum CornerNavigatorPlacement
|
||||
{
|
||||
/// <summary>
|
||||
/// Top-left corner of the screen.
|
||||
/// </summary>
|
||||
TopLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Top-right corner of the screen.
|
||||
/// </summary>
|
||||
TopRight,
|
||||
|
||||
/// <summary>
|
||||
/// Bottom-left corner of the screen.
|
||||
/// </summary>
|
||||
BottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Bottom-right corner of the screen.
|
||||
/// </summary>
|
||||
BottomRight
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the positioning strategy for the callout card.
|
||||
/// </summary>
|
||||
public enum CalloutPositionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Callout follows/positions relative to the highlighted element.
|
||||
/// Uses CalloutPlacement to determine which side (Auto, Top, Bottom, Left, Right).
|
||||
/// </summary>
|
||||
Following,
|
||||
|
||||
/// <summary>
|
||||
/// Callout is fixed in a specific screen corner.
|
||||
/// Uses CalloutCorner to determine which corner.
|
||||
/// </summary>
|
||||
FixedCorner,
|
||||
|
||||
/// <summary>
|
||||
/// Callout automatically positions in the screen corner that least
|
||||
/// interferes with the highlighted element.
|
||||
/// </summary>
|
||||
AutoCorner
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies which screen corner for fixed/auto corner positioning.
|
||||
/// </summary>
|
||||
public enum CalloutCorner
|
||||
{
|
||||
/// <summary>
|
||||
/// Top-left corner of the screen.
|
||||
/// </summary>
|
||||
TopLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Top-right corner of the screen.
|
||||
/// </summary>
|
||||
TopRight,
|
||||
|
||||
/// <summary>
|
||||
/// Bottom-left corner of the screen.
|
||||
/// </summary>
|
||||
BottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Bottom-right corner of the screen.
|
||||
/// </summary>
|
||||
BottomRight
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the display mode for the onboarding tour.
|
||||
/// </summary>
|
||||
public enum TourDisplayMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Full experience: dimmed overlay + spotlight cutout + callout card with nav buttons.
|
||||
/// </summary>
|
||||
SpotlightWithCallout,
|
||||
|
||||
/// <summary>
|
||||
/// Callout cards only - no dimming, just floating callouts that point to elements.
|
||||
/// Good for light-touch guidance without blocking interaction.
|
||||
/// </summary>
|
||||
CalloutOnly,
|
||||
|
||||
/// <summary>
|
||||
/// Dimmed overlay with spotlight cutouts, but titles/descriptions shown as
|
||||
/// inline labels positioned around the spotlight (no callout card).
|
||||
/// Use corner navigator for navigation.
|
||||
/// </summary>
|
||||
SpotlightWithInlineLabel
|
||||
}
|
||||
296
MarketAlly.MASpotlightTour/InlineLabel.cs
Normal file
296
MarketAlly.MASpotlightTour/InlineLabel.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// A simple label that displays title/description inline near the spotlight.
|
||||
/// Used in SpotlightWithInlineLabel mode.
|
||||
/// </summary>
|
||||
public class InlineLabel : Border
|
||||
{
|
||||
private readonly Label _titleLabel;
|
||||
private readonly Label _descriptionLabel;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
public static readonly BindableProperty TitleProperty =
|
||||
BindableProperty.Create(nameof(Title), typeof(string), typeof(InlineLabel), null,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b)._titleLabel.Text = (string?)n);
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => (string?)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DescriptionProperty =
|
||||
BindableProperty.Create(nameof(Description), typeof(string), typeof(InlineLabel), null,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b)._descriptionLabel.Text = (string?)n);
|
||||
|
||||
public string? Description
|
||||
{
|
||||
get => (string?)GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowDescriptionProperty =
|
||||
BindableProperty.Create(nameof(ShowDescription), typeof(bool), typeof(InlineLabel), true,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b)._descriptionLabel.IsVisible = (bool)n);
|
||||
|
||||
public bool ShowDescription
|
||||
{
|
||||
get => (bool)GetValue(ShowDescriptionProperty);
|
||||
set => SetValue(ShowDescriptionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty LabelBackgroundColorProperty =
|
||||
BindableProperty.Create(nameof(LabelBackgroundColor), typeof(Color), typeof(InlineLabel), Colors.White,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b).BackgroundColor = (Color)n);
|
||||
|
||||
public Color LabelBackgroundColor
|
||||
{
|
||||
get => (Color)GetValue(LabelBackgroundColorProperty);
|
||||
set => SetValue(LabelBackgroundColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty TitleColorProperty =
|
||||
BindableProperty.Create(nameof(TitleColor), typeof(Color), typeof(InlineLabel), Colors.Black,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b)._titleLabel.TextColor = (Color)n);
|
||||
|
||||
public Color TitleColor
|
||||
{
|
||||
get => (Color)GetValue(TitleColorProperty);
|
||||
set => SetValue(TitleColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DescriptionColorProperty =
|
||||
BindableProperty.Create(nameof(DescriptionColor), typeof(Color), typeof(InlineLabel), Colors.DarkGray,
|
||||
propertyChanged: (b, _, n) => ((InlineLabel)b)._descriptionLabel.TextColor = (Color)n);
|
||||
|
||||
public Color DescriptionColor
|
||||
{
|
||||
get => (Color)GetValue(DescriptionColorProperty);
|
||||
set => SetValue(DescriptionColorProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public InlineLabel()
|
||||
{
|
||||
BackgroundColor = Colors.White;
|
||||
StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 };
|
||||
Stroke = Colors.Transparent;
|
||||
Padding = new Thickness(12, 8);
|
||||
HorizontalOptions = LayoutOptions.Start;
|
||||
VerticalOptions = LayoutOptions.Start;
|
||||
MaximumWidthRequest = 300;
|
||||
|
||||
Shadow = new Shadow
|
||||
{
|
||||
Brush = new SolidColorBrush(Colors.Black),
|
||||
Offset = new Point(0, 2),
|
||||
Radius = 4,
|
||||
Opacity = 0.25f
|
||||
};
|
||||
|
||||
_titleLabel = new Label
|
||||
{
|
||||
FontAttributes = FontAttributes.Bold,
|
||||
FontSize = 16,
|
||||
TextColor = Colors.Black,
|
||||
LineBreakMode = LineBreakMode.WordWrap
|
||||
};
|
||||
|
||||
_descriptionLabel = new Label
|
||||
{
|
||||
FontSize = 13,
|
||||
TextColor = Colors.DarkGray,
|
||||
LineBreakMode = LineBreakMode.WordWrap
|
||||
};
|
||||
|
||||
Content = new VerticalStackLayout
|
||||
{
|
||||
Spacing = 4,
|
||||
Children = { _titleLabel, _descriptionLabel }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the label content.
|
||||
/// </summary>
|
||||
public void SetContent(string? title, string? description)
|
||||
{
|
||||
Title = title;
|
||||
Description = description;
|
||||
_titleLabel.IsVisible = !string.IsNullOrWhiteSpace(title);
|
||||
_descriptionLabel.IsVisible = ShowDescription && !string.IsNullOrWhiteSpace(description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the label relative to a spotlight rectangle.
|
||||
/// </summary>
|
||||
public void PositionRelativeToSpotlight(
|
||||
Rect spotlightRect,
|
||||
Size containerSize,
|
||||
CalloutPlacement placement,
|
||||
double margin = 8)
|
||||
{
|
||||
var effectivePlacement = placement == CalloutPlacement.Auto
|
||||
? DetermineAutoPlacement(spotlightRect, containerSize, margin)
|
||||
: placement;
|
||||
|
||||
double x, y;
|
||||
|
||||
switch (effectivePlacement)
|
||||
{
|
||||
case CalloutPlacement.Top:
|
||||
x = Math.Max(margin, Math.Min(spotlightRect.Center.X - 100, containerSize.Width - 200 - margin));
|
||||
y = Math.Max(margin, spotlightRect.Top - 60 - margin);
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Bottom:
|
||||
x = Math.Max(margin, Math.Min(spotlightRect.Center.X - 100, containerSize.Width - 200 - margin));
|
||||
y = Math.Min(containerSize.Height - 60 - margin, spotlightRect.Bottom + margin);
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Left:
|
||||
x = Math.Max(margin, spotlightRect.Left - 200 - margin);
|
||||
y = Math.Max(margin, Math.Min(spotlightRect.Center.Y - 30, containerSize.Height - 60 - margin));
|
||||
break;
|
||||
|
||||
case CalloutPlacement.Right:
|
||||
x = Math.Min(containerSize.Width - 200 - margin, spotlightRect.Right + margin);
|
||||
y = Math.Max(margin, Math.Min(spotlightRect.Center.Y - 30, containerSize.Height - 60 - margin));
|
||||
break;
|
||||
|
||||
default:
|
||||
x = spotlightRect.Center.X - 100;
|
||||
y = spotlightRect.Bottom + margin;
|
||||
break;
|
||||
}
|
||||
|
||||
TranslationX = x;
|
||||
TranslationY = y;
|
||||
}
|
||||
|
||||
private static CalloutPlacement DetermineAutoPlacement(Rect spotlightRect, Size containerSize, double margin)
|
||||
{
|
||||
var spaceBelow = containerSize.Height - spotlightRect.Bottom - margin;
|
||||
var spaceAbove = spotlightRect.Top - margin;
|
||||
|
||||
if (spaceBelow >= 70)
|
||||
return CalloutPlacement.Bottom;
|
||||
if (spaceAbove >= 70)
|
||||
return CalloutPlacement.Top;
|
||||
|
||||
return CalloutPlacement.Bottom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the label in a specific screen corner.
|
||||
/// </summary>
|
||||
public void PositionInCorner(CalloutCorner corner, Size containerSize, double margin = 16)
|
||||
{
|
||||
var measured = Measure(MaximumWidthRequest, double.PositiveInfinity);
|
||||
var labelWidth = Math.Min(measured.Width, MaximumWidthRequest);
|
||||
var labelHeight = measured.Height;
|
||||
|
||||
double x, y;
|
||||
|
||||
switch (corner)
|
||||
{
|
||||
case CalloutCorner.TopLeft:
|
||||
x = margin;
|
||||
y = margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.TopRight:
|
||||
x = containerSize.Width - labelWidth - margin;
|
||||
y = margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.BottomLeft:
|
||||
x = margin;
|
||||
y = containerSize.Height - labelHeight - margin;
|
||||
break;
|
||||
|
||||
case CalloutCorner.BottomRight:
|
||||
default:
|
||||
x = containerSize.Width - labelWidth - margin;
|
||||
y = containerSize.Height - labelHeight - margin;
|
||||
break;
|
||||
}
|
||||
|
||||
TranslationX = x;
|
||||
TranslationY = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the best corner that doesn't overlap with the spotlight area.
|
||||
/// </summary>
|
||||
public static CalloutCorner DetermineBestCorner(Rect spotlightRect, Size containerSize, double labelWidth, double labelHeight, double margin = 16)
|
||||
{
|
||||
var corners = new[]
|
||||
{
|
||||
CalloutCorner.TopLeft,
|
||||
CalloutCorner.TopRight,
|
||||
CalloutCorner.BottomLeft,
|
||||
CalloutCorner.BottomRight
|
||||
};
|
||||
|
||||
CalloutCorner bestCorner = CalloutCorner.BottomLeft;
|
||||
double minOverlap = double.MaxValue;
|
||||
|
||||
foreach (var corner in corners)
|
||||
{
|
||||
var labelRect = GetCornerRect(corner, containerSize, labelWidth, labelHeight, margin);
|
||||
var overlap = CalculateOverlap(labelRect, spotlightRect);
|
||||
|
||||
if (overlap < minOverlap)
|
||||
{
|
||||
minOverlap = overlap;
|
||||
bestCorner = corner;
|
||||
}
|
||||
}
|
||||
|
||||
return bestCorner;
|
||||
}
|
||||
|
||||
private static Rect GetCornerRect(CalloutCorner corner, Size containerSize, double width, double height, double margin)
|
||||
{
|
||||
return corner switch
|
||||
{
|
||||
CalloutCorner.TopLeft => new Rect(margin, margin, width, height),
|
||||
CalloutCorner.TopRight => new Rect(containerSize.Width - width - margin, margin, width, height),
|
||||
CalloutCorner.BottomLeft => new Rect(margin, containerSize.Height - height - margin, width, height),
|
||||
CalloutCorner.BottomRight => new Rect(containerSize.Width - width - margin, containerSize.Height - height - margin, width, height),
|
||||
_ => new Rect(margin, containerSize.Height - height - margin, width, height)
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateOverlap(Rect rect1, Rect rect2)
|
||||
{
|
||||
var xOverlap = Math.Max(0, Math.Min(rect1.Right, rect2.Right) - Math.Max(rect1.Left, rect2.Left));
|
||||
var yOverlap = Math.Max(0, Math.Min(rect1.Bottom, rect2.Bottom) - Math.Max(rect1.Top, rect2.Top));
|
||||
return xOverlap * yOverlap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies light or dark theme to the inline label.
|
||||
/// </summary>
|
||||
public void ApplyTheme(bool isDark)
|
||||
{
|
||||
if (isDark)
|
||||
{
|
||||
LabelBackgroundColor = Color.FromArgb("#1C1C1E");
|
||||
TitleColor = Colors.White;
|
||||
DescriptionColor = Color.FromArgb("#ABABAB");
|
||||
Stroke = Color.FromArgb("#3A3A3C");
|
||||
}
|
||||
else
|
||||
{
|
||||
LabelBackgroundColor = Colors.White;
|
||||
TitleColor = Colors.Black;
|
||||
DescriptionColor = Colors.DarkGray;
|
||||
Stroke = Colors.Transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj
Normal file
27
MarketAlly.MASpotlightTour/MASpotlightTour.Maui.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>MarketAlly.MASpotlightTour</AssemblyName>
|
||||
<RootNamespace>MarketAlly.MASpotlightTour</RootNamespace>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
253
MarketAlly.MASpotlightTour/Onboarding.cs
Normal file
253
MarketAlly.MASpotlightTour/Onboarding.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// Provides attached properties for tagging UI elements as onboarding/tour steps.
|
||||
/// </summary>
|
||||
public static class Onboarding
|
||||
{
|
||||
#region StepKey
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the step with a unique key. Falls back to AutomationId if not set.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty StepKeyProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"StepKey",
|
||||
typeof(string),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static string? GetStepKey(BindableObject view) =>
|
||||
(string?)view.GetValue(StepKeyProperty);
|
||||
|
||||
public static void SetStepKey(BindableObject view, string? value) =>
|
||||
view.SetValue(StepKeyProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Title
|
||||
|
||||
/// <summary>
|
||||
/// The title text displayed in the callout for this step.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TitleProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"Title",
|
||||
typeof(string),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static string? GetTitle(BindableObject view) =>
|
||||
(string?)view.GetValue(TitleProperty);
|
||||
|
||||
public static void SetTitle(BindableObject view, string? value) =>
|
||||
view.SetValue(TitleProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Description
|
||||
|
||||
/// <summary>
|
||||
/// The description text displayed in the callout for this step.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty DescriptionProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"Description",
|
||||
typeof(string),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static string? GetDescription(BindableObject view) =>
|
||||
(string?)view.GetValue(DescriptionProperty);
|
||||
|
||||
public static void SetDescription(BindableObject view, string? value) =>
|
||||
view.SetValue(DescriptionProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Order
|
||||
|
||||
/// <summary>
|
||||
/// The order/sequence number for this step within its group.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty OrderProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"Order",
|
||||
typeof(int),
|
||||
typeof(Onboarding),
|
||||
defaultValue: 0);
|
||||
|
||||
public static int GetOrder(BindableObject view) =>
|
||||
(int)view.GetValue(OrderProperty);
|
||||
|
||||
public static void SetOrder(BindableObject view, int value) =>
|
||||
view.SetValue(OrderProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Group
|
||||
|
||||
/// <summary>
|
||||
/// The group name for organizing multiple tours on the same page.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty GroupProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"Group",
|
||||
typeof(string),
|
||||
typeof(Onboarding),
|
||||
defaultValue: null);
|
||||
|
||||
public static string? GetGroup(BindableObject view) =>
|
||||
(string?)view.GetValue(GroupProperty);
|
||||
|
||||
public static void SetGroup(BindableObject view, string? value) =>
|
||||
view.SetValue(GroupProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpotlightEnabled
|
||||
|
||||
/// <summary>
|
||||
/// Whether this element participates in spotlight cutouts.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty SpotlightEnabledProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"SpotlightEnabled",
|
||||
typeof(bool),
|
||||
typeof(Onboarding),
|
||||
defaultValue: true);
|
||||
|
||||
public static bool GetSpotlightEnabled(BindableObject view) =>
|
||||
(bool)view.GetValue(SpotlightEnabledProperty);
|
||||
|
||||
public static void SetSpotlightEnabled(BindableObject view, bool value) =>
|
||||
view.SetValue(SpotlightEnabledProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Placement
|
||||
|
||||
/// <summary>
|
||||
/// Where to place the callout card relative to the spotlight.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty PlacementProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"Placement",
|
||||
typeof(CalloutPlacement),
|
||||
typeof(Onboarding),
|
||||
defaultValue: CalloutPlacement.Auto);
|
||||
|
||||
public static CalloutPlacement GetPlacement(BindableObject view) =>
|
||||
(CalloutPlacement)view.GetValue(PlacementProperty);
|
||||
|
||||
public static void SetPlacement(BindableObject view, CalloutPlacement value) =>
|
||||
view.SetValue(PlacementProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpotlightShape
|
||||
|
||||
/// <summary>
|
||||
/// The shape of the spotlight cutout for this step.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty SpotlightShapeProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"SpotlightShape",
|
||||
typeof(SpotlightShape),
|
||||
typeof(Onboarding),
|
||||
defaultValue: SpotlightShape.RoundedRectangle);
|
||||
|
||||
public static SpotlightShape GetSpotlightShape(BindableObject view) =>
|
||||
(SpotlightShape)view.GetValue(SpotlightShapeProperty);
|
||||
|
||||
public static void SetSpotlightShape(BindableObject view, SpotlightShape value) =>
|
||||
view.SetValue(SpotlightShapeProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpotlightPadding
|
||||
|
||||
/// <summary>
|
||||
/// Padding around the target element in the spotlight cutout.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty SpotlightPaddingProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"SpotlightPadding",
|
||||
typeof(Thickness),
|
||||
typeof(Onboarding),
|
||||
defaultValue: new Thickness(8));
|
||||
|
||||
public static Thickness GetSpotlightPadding(BindableObject view) =>
|
||||
(Thickness)view.GetValue(SpotlightPaddingProperty);
|
||||
|
||||
public static void SetSpotlightPadding(BindableObject view, Thickness value) =>
|
||||
view.SetValue(SpotlightPaddingProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region SpotlightCornerRadius
|
||||
|
||||
/// <summary>
|
||||
/// Corner radius for RoundedRectangle spotlight shape.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty SpotlightCornerRadiusProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"SpotlightCornerRadius",
|
||||
typeof(double),
|
||||
typeof(Onboarding),
|
||||
defaultValue: 8.0);
|
||||
|
||||
public static double GetSpotlightCornerRadius(BindableObject view) =>
|
||||
(double)view.GetValue(SpotlightCornerRadiusProperty);
|
||||
|
||||
public static void SetSpotlightCornerRadius(BindableObject view, double value) =>
|
||||
view.SetValue(SpotlightCornerRadiusProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
#region TapBehavior
|
||||
|
||||
/// <summary>
|
||||
/// Behavior when the user taps on the spotlighted element.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty TapBehaviorProperty =
|
||||
BindableProperty.CreateAttached(
|
||||
"TapBehavior",
|
||||
typeof(SpotlightTapBehavior),
|
||||
typeof(Onboarding),
|
||||
defaultValue: SpotlightTapBehavior.None);
|
||||
|
||||
public static SpotlightTapBehavior GetTapBehavior(BindableObject view) =>
|
||||
(SpotlightTapBehavior)view.GetValue(TapBehaviorProperty);
|
||||
|
||||
public static void SetTapBehavior(BindableObject view, SpotlightTapBehavior value) =>
|
||||
view.SetValue(TapBehaviorProperty, value);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Helper to get the effective step key, falling back to AutomationId.
|
||||
/// </summary>
|
||||
public static string? GetEffectiveStepKey(BindableObject view)
|
||||
{
|
||||
var stepKey = GetStepKey(view);
|
||||
if (!string.IsNullOrWhiteSpace(stepKey))
|
||||
return stepKey;
|
||||
|
||||
if (view is VisualElement ve && !string.IsNullOrWhiteSpace(ve.AutomationId))
|
||||
return ve.AutomationId;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if an element is tagged as an onboarding step.
|
||||
/// </summary>
|
||||
public static bool IsOnboardingStep(BindableObject view)
|
||||
{
|
||||
// An element is considered a step if it has a StepKey, Title, or Description set
|
||||
return !string.IsNullOrWhiteSpace(GetStepKey(view)) ||
|
||||
!string.IsNullOrWhiteSpace(GetTitle(view)) ||
|
||||
!string.IsNullOrWhiteSpace(GetDescription(view));
|
||||
}
|
||||
}
|
||||
934
MarketAlly.MASpotlightTour/OnboardingHost.cs
Normal file
934
MarketAlly.MASpotlightTour/OnboardingHost.cs
Normal file
@@ -0,0 +1,934 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// The main host control that orchestrates the onboarding tour experience.
|
||||
/// Add this as an overlay on your page to enable spotlight tours.
|
||||
/// </summary>
|
||||
public class OnboardingHost : Grid
|
||||
{
|
||||
private readonly SpotlightOverlay _spotlightOverlay;
|
||||
private readonly CalloutCard _calloutCard;
|
||||
private readonly CornerNavigator _cornerNavigator;
|
||||
private readonly ArrowIndicator _arrowIndicator;
|
||||
private readonly InlineLabel _inlineLabel;
|
||||
|
||||
private IReadOnlyList<OnboardingStep> _steps = Array.Empty<OnboardingStep>();
|
||||
private int _currentIndex = -1;
|
||||
private VisualElement? _root;
|
||||
private bool _isRunning;
|
||||
private CancellationTokenSource? _autoAdvanceCts;
|
||||
|
||||
#region Bindable Properties
|
||||
|
||||
/// <summary>
|
||||
/// The display mode for the tour (SpotlightWithCallout, CalloutOnly, SpotlightWithInlineLabel).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty DisplayModeProperty =
|
||||
BindableProperty.Create(nameof(DisplayMode), typeof(TourDisplayMode), typeof(OnboardingHost),
|
||||
TourDisplayMode.SpotlightWithCallout);
|
||||
|
||||
public TourDisplayMode DisplayMode
|
||||
{
|
||||
get => (TourDisplayMode)GetValue(DisplayModeProperty);
|
||||
set => SetValue(DisplayModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowCornerNavigatorProperty =
|
||||
BindableProperty.Create(nameof(ShowCornerNavigator), typeof(bool), typeof(OnboardingHost), true,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._cornerNavigator.IsVisible = (bool)n && ((OnboardingHost)b)._isRunning);
|
||||
|
||||
public bool ShowCornerNavigator
|
||||
{
|
||||
get => (bool)GetValue(ShowCornerNavigatorProperty);
|
||||
set => SetValue(ShowCornerNavigatorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty CornerNavigatorPlacementProperty =
|
||||
BindableProperty.Create(nameof(CornerNavigatorPlacement), typeof(CornerNavigatorPlacement), typeof(OnboardingHost),
|
||||
CornerNavigatorPlacement.BottomRight,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._cornerNavigator.Placement = (CornerNavigatorPlacement)n);
|
||||
|
||||
public CornerNavigatorPlacement CornerNavigatorPlacement
|
||||
{
|
||||
get => (CornerNavigatorPlacement)GetValue(CornerNavigatorPlacementProperty);
|
||||
set => SetValue(CornerNavigatorPlacementProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowSkipButtonProperty =
|
||||
BindableProperty.Create(nameof(ShowSkipButton), typeof(bool), typeof(OnboardingHost), true,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._cornerNavigator.ShowSkipButton = (bool)n);
|
||||
|
||||
public bool ShowSkipButton
|
||||
{
|
||||
get => (bool)GetValue(ShowSkipButtonProperty);
|
||||
set => SetValue(ShowSkipButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty ShowArrowProperty =
|
||||
BindableProperty.Create(nameof(ShowArrow), typeof(bool), typeof(OnboardingHost), true,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._arrowIndicator.IsVisible = (bool)n && ((OnboardingHost)b)._isRunning);
|
||||
|
||||
public bool ShowArrow
|
||||
{
|
||||
get => (bool)GetValue(ShowArrowProperty);
|
||||
set => SetValue(ShowArrowProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DimOpacityProperty =
|
||||
BindableProperty.Create(nameof(DimOpacity), typeof(double), typeof(OnboardingHost), 0.6,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._spotlightOverlay.DimOpacity = (double)n);
|
||||
|
||||
public double DimOpacity
|
||||
{
|
||||
get => (double)GetValue(DimOpacityProperty);
|
||||
set => SetValue(DimOpacityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty DimColorProperty =
|
||||
BindableProperty.Create(nameof(DimColor), typeof(Color), typeof(OnboardingHost), Colors.Black,
|
||||
propertyChanged: (b, _, n) => ((OnboardingHost)b)._spotlightOverlay.DimColor = (Color)n);
|
||||
|
||||
public Color DimColor
|
||||
{
|
||||
get => (Color)GetValue(DimColorProperty);
|
||||
set => SetValue(DimColorProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty AnimationDurationProperty =
|
||||
BindableProperty.Create(nameof(AnimationDuration), typeof(uint), typeof(OnboardingHost), (uint)250);
|
||||
|
||||
public uint AnimationDuration
|
||||
{
|
||||
get => (uint)GetValue(AnimationDurationProperty);
|
||||
set => SetValue(AnimationDurationProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The positioning strategy for the callout card (Following, FixedCorner, AutoCorner).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty CalloutPositionModeProperty =
|
||||
BindableProperty.Create(nameof(CalloutPositionMode), typeof(CalloutPositionMode), typeof(OnboardingHost),
|
||||
CalloutPositionMode.Following);
|
||||
|
||||
public CalloutPositionMode CalloutPositionMode
|
||||
{
|
||||
get => (CalloutPositionMode)GetValue(CalloutPositionModeProperty);
|
||||
set => SetValue(CalloutPositionModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The corner to use when CalloutPositionMode is FixedCorner.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty CalloutCornerProperty =
|
||||
BindableProperty.Create(nameof(CalloutCorner), typeof(CalloutCorner), typeof(OnboardingHost),
|
||||
CalloutCorner.BottomLeft);
|
||||
|
||||
public CalloutCorner CalloutCorner
|
||||
{
|
||||
get => (CalloutCorner)GetValue(CalloutCornerProperty);
|
||||
set => SetValue(CalloutCornerProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Margin from screen edge when using corner positioning (in device-independent units).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty CalloutCornerMarginProperty =
|
||||
BindableProperty.Create(nameof(CalloutCornerMargin), typeof(double), typeof(OnboardingHost), 16.0);
|
||||
|
||||
public double CalloutCornerMargin
|
||||
{
|
||||
get => (double)GetValue(CalloutCornerMarginProperty);
|
||||
set => SetValue(CalloutCornerMarginProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The visual theme for the tour (Light, Dark, or System).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty ThemeProperty =
|
||||
BindableProperty.Create(nameof(Theme), typeof(TourTheme), typeof(OnboardingHost),
|
||||
TourTheme.Light,
|
||||
propertyChanged: (b, _, _) => ((OnboardingHost)b).ApplyTheme());
|
||||
|
||||
public TourTheme Theme
|
||||
{
|
||||
get => (TourTheme)GetValue(ThemeProperty);
|
||||
set => SetValue(ThemeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly BindableProperty AnimationsEnabledProperty =
|
||||
BindableProperty.Create(nameof(AnimationsEnabled), typeof(bool), typeof(OnboardingHost), true);
|
||||
|
||||
public bool AnimationsEnabled
|
||||
{
|
||||
get => (bool)GetValue(AnimationsEnabledProperty);
|
||||
set => SetValue(AnimationsEnabledProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional delay in milliseconds before auto-starting the tour.
|
||||
/// Set to 0 or negative to disable auto-start (default).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty AutoStartDelayProperty =
|
||||
BindableProperty.Create(nameof(AutoStartDelay), typeof(int), typeof(OnboardingHost), 0);
|
||||
|
||||
public int AutoStartDelay
|
||||
{
|
||||
get => (int)GetValue(AutoStartDelayProperty);
|
||||
set => SetValue(AutoStartDelayProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The group to use for auto-start. If null, all steps are included.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty AutoStartGroupProperty =
|
||||
BindableProperty.Create(nameof(AutoStartGroup), typeof(string), typeof(OnboardingHost), null);
|
||||
|
||||
public string? AutoStartGroup
|
||||
{
|
||||
get => (string?)GetValue(AutoStartGroupProperty);
|
||||
set => SetValue(AutoStartGroupProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optional delay in milliseconds before auto-advancing to the next step.
|
||||
/// Set to 0 or negative to disable auto-advance (default).
|
||||
/// When enabled, the tour automatically moves to the next step after this delay.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty AutoAdvanceDelayProperty =
|
||||
BindableProperty.Create(nameof(AutoAdvanceDelay), typeof(int), typeof(OnboardingHost), 0);
|
||||
|
||||
public int AutoAdvanceDelay
|
||||
{
|
||||
get => (int)GetValue(AutoAdvanceDelayProperty);
|
||||
set => SetValue(AutoAdvanceDelayProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the tour starts.
|
||||
/// </summary>
|
||||
public event EventHandler? TourStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the tour completes (user reaches the end).
|
||||
/// </summary>
|
||||
public event EventHandler? TourCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the tour is skipped.
|
||||
/// </summary>
|
||||
public event EventHandler? TourSkipped;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when moving to a new step.
|
||||
/// </summary>
|
||||
public event EventHandler<OnboardingStepEventArgs>? StepChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the tour ends (either completed or skipped).
|
||||
/// </summary>
|
||||
public event EventHandler? TourEnded;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read-only Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a tour is currently running.
|
||||
/// </summary>
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current step index (0-based).
|
||||
/// </summary>
|
||||
public int CurrentStepIndex => _currentIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of steps.
|
||||
/// </summary>
|
||||
public int TotalSteps => _steps.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current step, or null if no tour is running.
|
||||
/// </summary>
|
||||
public OnboardingStep? CurrentStep => _currentIndex >= 0 && _currentIndex < _steps.Count
|
||||
? _steps[_currentIndex]
|
||||
: null;
|
||||
|
||||
#endregion
|
||||
|
||||
public OnboardingHost()
|
||||
{
|
||||
IsVisible = false;
|
||||
InputTransparent = false;
|
||||
BackgroundColor = Colors.Transparent;
|
||||
HorizontalOptions = LayoutOptions.Fill;
|
||||
VerticalOptions = LayoutOptions.Fill;
|
||||
|
||||
_spotlightOverlay = new SpotlightOverlay
|
||||
{
|
||||
IsVisible = false,
|
||||
HorizontalOptions = LayoutOptions.Fill,
|
||||
VerticalOptions = LayoutOptions.Fill
|
||||
};
|
||||
_calloutCard = new CalloutCard
|
||||
{
|
||||
IsVisible = false,
|
||||
HorizontalOptions = LayoutOptions.Start,
|
||||
VerticalOptions = LayoutOptions.Start
|
||||
};
|
||||
_arrowIndicator = new ArrowIndicator
|
||||
{
|
||||
IsVisible = false,
|
||||
HorizontalOptions = LayoutOptions.Start,
|
||||
VerticalOptions = LayoutOptions.Start
|
||||
};
|
||||
_inlineLabel = new InlineLabel
|
||||
{
|
||||
IsVisible = false,
|
||||
HorizontalOptions = LayoutOptions.Start,
|
||||
VerticalOptions = LayoutOptions.Start
|
||||
};
|
||||
_cornerNavigator = new CornerNavigator { IsVisible = false };
|
||||
|
||||
// Wire up events
|
||||
_calloutCard.PreviousClicked += (_, _) => GoToPreviousStep();
|
||||
_calloutCard.NextClicked += (_, _) => GoToNextStep();
|
||||
_calloutCard.CloseClicked += (_, _) => CompleteTour();
|
||||
|
||||
_cornerNavigator.PreviousClicked += (_, _) => GoToPreviousStep();
|
||||
_cornerNavigator.NextClicked += (_, _) => GoToNextStep();
|
||||
_cornerNavigator.SkipClicked += (_, _) => SkipTour();
|
||||
|
||||
_spotlightOverlay.SpotlightTapped += OnSpotlightTapped;
|
||||
_spotlightOverlay.DimmedAreaTapped += OnDimmedAreaTapped;
|
||||
|
||||
// Layer order: spotlight overlay (bottom), arrow, inline label, callout card, corner navigator (top)
|
||||
Children.Add(_spotlightOverlay);
|
||||
Children.Add(_arrowIndicator);
|
||||
Children.Add(_inlineLabel);
|
||||
Children.Add(_calloutCard);
|
||||
Children.Add(_cornerNavigator);
|
||||
|
||||
// Handle auto-start when added to visual tree
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private async void OnLoaded(object? sender, EventArgs e)
|
||||
{
|
||||
Loaded -= OnLoaded;
|
||||
|
||||
if (AutoStartDelay > 0)
|
||||
{
|
||||
await Task.Delay(AutoStartDelay);
|
||||
await AutoStartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutoStartAsync()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
// Find the root - walk up to find Page or use parent
|
||||
var root = FindAutoStartRoot();
|
||||
if (root == null)
|
||||
return;
|
||||
|
||||
await StartTourAsync(root, AutoStartGroup);
|
||||
}
|
||||
|
||||
private VisualElement? FindAutoStartRoot()
|
||||
{
|
||||
Element? current = Parent;
|
||||
VisualElement? lastVisual = null;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
if (current is ContentPage contentPage)
|
||||
return contentPage.Content as VisualElement ?? contentPage;
|
||||
|
||||
if (current is VisualElement ve)
|
||||
lastVisual = ve;
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return lastVisual;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops any currently running tour.
|
||||
/// </summary>
|
||||
public void StopTour()
|
||||
{
|
||||
if (_isRunning)
|
||||
EndTour();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the onboarding tour by scanning for tagged elements.
|
||||
/// </summary>
|
||||
/// <param name="root">The root element to scan (typically the Page or a layout).</param>
|
||||
/// <param name="group">Optional group filter.</param>
|
||||
public async Task StartTourAsync(VisualElement root, string? group = null)
|
||||
{
|
||||
// Stop any existing tour first
|
||||
if (_isRunning)
|
||||
EndTour();
|
||||
|
||||
_root = root;
|
||||
_steps = OnboardingScanner.FindSteps(root, group);
|
||||
|
||||
if (_steps.Count == 0)
|
||||
return;
|
||||
|
||||
_isRunning = true;
|
||||
_currentIndex = -1;
|
||||
_cornerNavigator.TotalSteps = _steps.Count;
|
||||
|
||||
// Apply current theme to all components
|
||||
ApplyTheme();
|
||||
|
||||
// Ensure layout is complete before positioning
|
||||
await WaitForLayoutAsync();
|
||||
|
||||
TourStarted?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// Position first step before showing anything
|
||||
await GoToStepAsync(0);
|
||||
|
||||
// Now show everything based on display mode
|
||||
ShowElementsForDisplayMode();
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
private void ShowElementsForDisplayMode()
|
||||
{
|
||||
// Hide callout card buttons when corner navigator is visible (avoid duplicate nav)
|
||||
_calloutCard.ShowNavigationButtons = !ShowCornerNavigator;
|
||||
|
||||
switch (DisplayMode)
|
||||
{
|
||||
case TourDisplayMode.SpotlightWithCallout:
|
||||
_spotlightOverlay.IsVisible = true;
|
||||
_calloutCard.IsVisible = true;
|
||||
_inlineLabel.IsVisible = false;
|
||||
_cornerNavigator.IsVisible = ShowCornerNavigator;
|
||||
InputTransparent = false;
|
||||
break;
|
||||
|
||||
case TourDisplayMode.CalloutOnly:
|
||||
_spotlightOverlay.IsVisible = false;
|
||||
_calloutCard.IsVisible = true;
|
||||
_inlineLabel.IsVisible = false;
|
||||
_cornerNavigator.IsVisible = ShowCornerNavigator;
|
||||
// Allow interaction with underlying elements in callout-only mode
|
||||
InputTransparent = true;
|
||||
_calloutCard.InputTransparent = false;
|
||||
_cornerNavigator.InputTransparent = false;
|
||||
break;
|
||||
|
||||
case TourDisplayMode.SpotlightWithInlineLabel:
|
||||
_spotlightOverlay.IsVisible = true;
|
||||
_calloutCard.IsVisible = false;
|
||||
_inlineLabel.IsVisible = true;
|
||||
_cornerNavigator.IsVisible = true; // Always show nav in this mode
|
||||
InputTransparent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForLayoutAsync()
|
||||
{
|
||||
// If already laid out, skip waiting
|
||||
if (Width > 0 && Height > 0)
|
||||
return;
|
||||
|
||||
// Make host temporarily visible to trigger layout
|
||||
IsVisible = true;
|
||||
|
||||
// Wait for layout pass
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
void OnSizeChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (Width > 0 && Height > 0)
|
||||
{
|
||||
SizeChanged -= OnSizeChanged;
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
// Timeout after 500ms
|
||||
var timeoutTask = Task.Delay(500);
|
||||
await Task.WhenAny(tcs.Task, timeoutTask);
|
||||
|
||||
SizeChanged -= OnSizeChanged;
|
||||
IsVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the tour from a specific step key.
|
||||
/// </summary>
|
||||
public async Task StartTourFromStepAsync(VisualElement root, string stepKey, string? group = null)
|
||||
{
|
||||
// Stop any existing tour first
|
||||
if (_isRunning)
|
||||
EndTour();
|
||||
|
||||
_root = root;
|
||||
_steps = OnboardingScanner.FindSteps(root, group);
|
||||
|
||||
if (_steps.Count == 0)
|
||||
return;
|
||||
|
||||
var startIndex = _steps
|
||||
.Select((s, i) => new { Step = s, Index = i })
|
||||
.FirstOrDefault(x => string.Equals(x.Step.StepKey, stepKey, StringComparison.OrdinalIgnoreCase))
|
||||
?.Index ?? 0;
|
||||
|
||||
_isRunning = true;
|
||||
_currentIndex = -1;
|
||||
_cornerNavigator.TotalSteps = _steps.Count;
|
||||
|
||||
// Apply current theme to all components
|
||||
ApplyTheme();
|
||||
|
||||
// Ensure layout is complete before positioning
|
||||
await WaitForLayoutAsync();
|
||||
|
||||
TourStarted?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
// Position first step before showing anything
|
||||
await GoToStepAsync(startIndex);
|
||||
|
||||
// Now show everything based on display mode
|
||||
ShowElementsForDisplayMode();
|
||||
IsVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to a specific step by index.
|
||||
/// </summary>
|
||||
public async Task GoToStepAsync(int index)
|
||||
{
|
||||
if (!_isRunning || index < 0 || index >= _steps.Count || _root == null)
|
||||
return;
|
||||
|
||||
var previousIndex = _currentIndex;
|
||||
_currentIndex = index;
|
||||
var step = _steps[index];
|
||||
|
||||
// Ensure element is visible (scroll if needed)
|
||||
await step.Target.EnsureVisibleAsync(AnimationsEnabled);
|
||||
|
||||
// Brief delay to allow scroll to settle (only if we scrolled)
|
||||
if (AnimationsEnabled)
|
||||
await Task.Delay(20);
|
||||
|
||||
// Calculate bounds
|
||||
var bounds = step.Target.GetAbsoluteBoundsWithPadding(_root, step.SpotlightPadding);
|
||||
|
||||
// Determine effective placement
|
||||
var placement = step.Placement;
|
||||
if (placement == CalloutPlacement.Auto)
|
||||
{
|
||||
placement = DetermineAutoPlacement(bounds);
|
||||
}
|
||||
|
||||
var isFirst = index == 0;
|
||||
var isLast = index == _steps.Count - 1;
|
||||
|
||||
// Handle different display modes
|
||||
switch (DisplayMode)
|
||||
{
|
||||
case TourDisplayMode.SpotlightWithCallout:
|
||||
await UpdateSpotlightWithCallout(step, bounds, placement, isFirst, isLast, previousIndex);
|
||||
break;
|
||||
|
||||
case TourDisplayMode.CalloutOnly:
|
||||
await UpdateCalloutOnly(step, bounds, placement, isFirst, isLast);
|
||||
break;
|
||||
|
||||
case TourDisplayMode.SpotlightWithInlineLabel:
|
||||
await UpdateSpotlightWithInlineLabel(step, bounds, placement, previousIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update corner navigator
|
||||
_cornerNavigator.UpdateForStep(index, _steps.Count);
|
||||
|
||||
// Raise event
|
||||
StepChanged?.Invoke(this, new OnboardingStepEventArgs(step, index, _steps.Count));
|
||||
|
||||
// Start auto-advance timer if enabled
|
||||
StartAutoAdvanceTimer();
|
||||
}
|
||||
|
||||
private void StartAutoAdvanceTimer()
|
||||
{
|
||||
// Cancel any existing timer
|
||||
CancelAutoAdvanceTimer();
|
||||
|
||||
if (AutoAdvanceDelay <= 0 || !_isRunning)
|
||||
return;
|
||||
|
||||
_autoAdvanceCts = new CancellationTokenSource();
|
||||
var token = _autoAdvanceCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(AutoAdvanceDelay, token);
|
||||
|
||||
if (!token.IsCancellationRequested && _isRunning)
|
||||
{
|
||||
// Must dispatch to UI thread
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
if (_isRunning)
|
||||
GoToNextStep();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Timer was cancelled, ignore
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void CancelAutoAdvanceTimer()
|
||||
{
|
||||
_autoAdvanceCts?.Cancel();
|
||||
_autoAdvanceCts?.Dispose();
|
||||
_autoAdvanceCts = null;
|
||||
}
|
||||
|
||||
private async Task UpdateSpotlightWithCallout(OnboardingStep step, Rect bounds, CalloutPlacement placement, bool isFirst, bool isLast, int previousIndex)
|
||||
{
|
||||
// Update spotlight
|
||||
if (AnimationsEnabled && previousIndex >= 0)
|
||||
{
|
||||
await _spotlightOverlay.AnimateToAsync(bounds, step.SpotlightShape, step.SpotlightCornerRadius, AnimationDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spotlightOverlay.SetSpotlight(bounds, step.SpotlightShape, step.SpotlightCornerRadius);
|
||||
}
|
||||
|
||||
// Update callout card content
|
||||
_calloutCard.UpdateForStep(step.Title, step.Description, isFirst, isLast);
|
||||
|
||||
// Position callout based on CalloutPositionMode
|
||||
PositionCalloutCard(bounds, placement);
|
||||
|
||||
// Position arrow (only useful in Following mode)
|
||||
if (ShowArrow && CalloutPositionMode == CalloutPositionMode.Following)
|
||||
{
|
||||
_arrowIndicator.IsVisible = true;
|
||||
_arrowIndicator.ArrowColor = _calloutCard.CardBackgroundColor;
|
||||
|
||||
var calloutBounds = new Rect(
|
||||
_calloutCard.TranslationX,
|
||||
_calloutCard.TranslationY,
|
||||
_calloutCard.Width > 0 ? _calloutCard.Width : _calloutCard.WidthRequest,
|
||||
_calloutCard.Height > 0 ? _calloutCard.Height : 100);
|
||||
|
||||
_arrowIndicator.PositionBetween(calloutBounds, bounds, placement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide arrow in corner modes
|
||||
_arrowIndicator.IsVisible = false;
|
||||
}
|
||||
|
||||
// Hide inline label in this mode
|
||||
_inlineLabel.IsVisible = false;
|
||||
}
|
||||
|
||||
private void PositionCalloutCard(Rect spotlightBounds, CalloutPlacement placement)
|
||||
{
|
||||
var containerSize = new Size(Width, Height);
|
||||
|
||||
switch (CalloutPositionMode)
|
||||
{
|
||||
case CalloutPositionMode.Following:
|
||||
_calloutCard.PositionRelativeToSpotlight(spotlightBounds, containerSize, placement);
|
||||
break;
|
||||
|
||||
case CalloutPositionMode.FixedCorner:
|
||||
_calloutCard.PositionInCorner(CalloutCorner, containerSize, CalloutCornerMargin);
|
||||
break;
|
||||
|
||||
case CalloutPositionMode.AutoCorner:
|
||||
var cardWidth = Math.Min(containerSize.Width * 0.85, 400);
|
||||
var measured = _calloutCard.Measure(cardWidth, double.PositiveInfinity);
|
||||
var bestCorner = CalloutCard.DetermineBestCorner(
|
||||
spotlightBounds, containerSize, cardWidth, measured.Height, CalloutCornerMargin);
|
||||
_calloutCard.PositionInCorner(bestCorner, containerSize, CalloutCornerMargin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateCalloutOnly(OnboardingStep step, Rect bounds, CalloutPlacement placement, bool isFirst, bool isLast)
|
||||
{
|
||||
// No spotlight/dimming in this mode
|
||||
_spotlightOverlay.ClearSpotlight();
|
||||
|
||||
// Update callout card content
|
||||
_calloutCard.UpdateForStep(step.Title, step.Description, isFirst, isLast);
|
||||
|
||||
// Position callout based on CalloutPositionMode
|
||||
PositionCalloutCard(bounds, placement);
|
||||
|
||||
// Position arrow pointing to target (only in Following mode)
|
||||
if (ShowArrow && CalloutPositionMode == CalloutPositionMode.Following)
|
||||
{
|
||||
_arrowIndicator.IsVisible = true;
|
||||
_arrowIndicator.ArrowColor = _calloutCard.CardBackgroundColor;
|
||||
|
||||
var calloutBounds = new Rect(
|
||||
_calloutCard.TranslationX,
|
||||
_calloutCard.TranslationY,
|
||||
_calloutCard.Width > 0 ? _calloutCard.Width : _calloutCard.WidthRequest,
|
||||
_calloutCard.Height > 0 ? _calloutCard.Height : 100);
|
||||
|
||||
_arrowIndicator.PositionBetween(calloutBounds, bounds, placement);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide arrow in corner modes
|
||||
_arrowIndicator.IsVisible = false;
|
||||
}
|
||||
|
||||
// Hide inline label in this mode
|
||||
_inlineLabel.IsVisible = false;
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task UpdateSpotlightWithInlineLabel(OnboardingStep step, Rect bounds, CalloutPlacement placement, int previousIndex)
|
||||
{
|
||||
// Update spotlight
|
||||
if (AnimationsEnabled && previousIndex >= 0)
|
||||
{
|
||||
await _spotlightOverlay.AnimateToAsync(bounds, step.SpotlightShape, step.SpotlightCornerRadius, AnimationDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spotlightOverlay.SetSpotlight(bounds, step.SpotlightShape, step.SpotlightCornerRadius);
|
||||
}
|
||||
|
||||
// Hide callout card, show inline label
|
||||
_calloutCard.IsVisible = false;
|
||||
_arrowIndicator.IsVisible = false;
|
||||
|
||||
_inlineLabel.SetContent(step.Title, step.Description);
|
||||
|
||||
// Position inline label based on CalloutPositionMode
|
||||
PositionInlineLabel(bounds, placement);
|
||||
|
||||
_inlineLabel.IsVisible = true;
|
||||
}
|
||||
|
||||
private void PositionInlineLabel(Rect spotlightBounds, CalloutPlacement placement)
|
||||
{
|
||||
var containerSize = new Size(Width, Height);
|
||||
|
||||
switch (CalloutPositionMode)
|
||||
{
|
||||
case CalloutPositionMode.Following:
|
||||
_inlineLabel.PositionRelativeToSpotlight(spotlightBounds, containerSize, placement);
|
||||
break;
|
||||
|
||||
case CalloutPositionMode.FixedCorner:
|
||||
_inlineLabel.PositionInCorner(CalloutCorner, containerSize, CalloutCornerMargin);
|
||||
break;
|
||||
|
||||
case CalloutPositionMode.AutoCorner:
|
||||
var measured = _inlineLabel.Measure(_inlineLabel.MaximumWidthRequest, double.PositiveInfinity);
|
||||
var bestCorner = InlineLabel.DetermineBestCorner(
|
||||
spotlightBounds, containerSize, measured.Width, measured.Height, CalloutCornerMargin);
|
||||
_inlineLabel.PositionInCorner(bestCorner, containerSize, CalloutCornerMargin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances to the next step.
|
||||
/// </summary>
|
||||
public async void GoToNextStep()
|
||||
{
|
||||
if (_currentIndex < _steps.Count - 1)
|
||||
{
|
||||
await GoToStepAsync(_currentIndex + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
CompleteTour();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns to the previous step.
|
||||
/// </summary>
|
||||
public async void GoToPreviousStep()
|
||||
{
|
||||
if (_currentIndex > 0)
|
||||
{
|
||||
await GoToStepAsync(_currentIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completes the tour (user finished all steps).
|
||||
/// </summary>
|
||||
public void CompleteTour()
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
EndTour();
|
||||
TourCompleted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips/cancels the tour.
|
||||
/// </summary>
|
||||
public void SkipTour()
|
||||
{
|
||||
if (!_isRunning)
|
||||
return;
|
||||
|
||||
EndTour();
|
||||
TourSkipped?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void EndTour()
|
||||
{
|
||||
CancelAutoAdvanceTimer();
|
||||
|
||||
_isRunning = false;
|
||||
_currentIndex = -1;
|
||||
_steps = Array.Empty<OnboardingStep>();
|
||||
_root = null;
|
||||
|
||||
IsVisible = false;
|
||||
InputTransparent = false;
|
||||
_spotlightOverlay.IsVisible = false;
|
||||
_calloutCard.IsVisible = false;
|
||||
_inlineLabel.IsVisible = false;
|
||||
_cornerNavigator.IsVisible = false;
|
||||
_arrowIndicator.IsVisible = false;
|
||||
_spotlightOverlay.ClearSpotlight();
|
||||
|
||||
TourEnded?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void OnSpotlightTapped(object? sender, EventArgs e)
|
||||
{
|
||||
var step = CurrentStep;
|
||||
if (step == null)
|
||||
return;
|
||||
|
||||
switch (step.TapBehavior)
|
||||
{
|
||||
case SpotlightTapBehavior.Advance:
|
||||
GoToNextStep();
|
||||
break;
|
||||
|
||||
case SpotlightTapBehavior.Close:
|
||||
SkipTour();
|
||||
break;
|
||||
|
||||
case SpotlightTapBehavior.AllowInteraction:
|
||||
// TODO: Pass through the tap to the underlying element
|
||||
break;
|
||||
|
||||
case SpotlightTapBehavior.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDimmedAreaTapped(object? sender, EventArgs e)
|
||||
{
|
||||
// Optionally advance or do nothing when tapping outside
|
||||
// Currently does nothing - could be made configurable
|
||||
}
|
||||
|
||||
private CalloutPlacement DetermineAutoPlacement(Rect spotlightRect)
|
||||
{
|
||||
var containerHeight = Height;
|
||||
var containerWidth = Width;
|
||||
var margin = 12.0;
|
||||
var estimatedCardHeight = 150.0;
|
||||
|
||||
var spaceBelow = containerHeight - spotlightRect.Bottom - margin;
|
||||
var spaceAbove = spotlightRect.Top - margin;
|
||||
var spaceRight = containerWidth - spotlightRect.Right - margin;
|
||||
var spaceLeft = spotlightRect.Left - margin;
|
||||
|
||||
if (spaceBelow >= estimatedCardHeight)
|
||||
return CalloutPlacement.Bottom;
|
||||
if (spaceAbove >= estimatedCardHeight)
|
||||
return CalloutPlacement.Top;
|
||||
if (spaceRight >= 200)
|
||||
return CalloutPlacement.Right;
|
||||
if (spaceLeft >= 200)
|
||||
return CalloutPlacement.Left;
|
||||
|
||||
return CalloutPlacement.Bottom;
|
||||
}
|
||||
|
||||
private void ApplyTheme()
|
||||
{
|
||||
var isDark = GetEffectiveTheme() == TourTheme.Dark;
|
||||
|
||||
// Apply theme to callout card
|
||||
_calloutCard.ApplyTheme(isDark);
|
||||
|
||||
// Apply theme to corner navigator
|
||||
_cornerNavigator.ApplyTheme(isDark);
|
||||
|
||||
// Apply theme to inline label
|
||||
_inlineLabel.ApplyTheme(isDark);
|
||||
}
|
||||
|
||||
private TourTheme GetEffectiveTheme()
|
||||
{
|
||||
if (Theme == TourTheme.System)
|
||||
{
|
||||
return Application.Current?.RequestedTheme == AppTheme.Dark
|
||||
? TourTheme.Dark
|
||||
: TourTheme.Light;
|
||||
}
|
||||
return Theme;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for step change events.
|
||||
/// </summary>
|
||||
public class OnboardingStepEventArgs : EventArgs
|
||||
{
|
||||
public OnboardingStep Step { get; }
|
||||
public int StepIndex { get; }
|
||||
public int TotalSteps { get; }
|
||||
|
||||
public OnboardingStepEventArgs(OnboardingStep step, int stepIndex, int totalSteps)
|
||||
{
|
||||
Step = step;
|
||||
StepIndex = stepIndex;
|
||||
TotalSteps = totalSteps;
|
||||
}
|
||||
}
|
||||
89
MarketAlly.MASpotlightTour/OnboardingScanner.cs
Normal file
89
MarketAlly.MASpotlightTour/OnboardingScanner.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// Scans the visual tree to find elements tagged with onboarding properties.
|
||||
/// </summary>
|
||||
public static class OnboardingScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds all onboarding steps within the given root element.
|
||||
/// </summary>
|
||||
/// <param name="root">The root element to scan.</param>
|
||||
/// <param name="group">Optional group filter. If null, returns all steps.</param>
|
||||
/// <returns>Ordered list of onboarding steps.</returns>
|
||||
public static IReadOnlyList<OnboardingStep> FindSteps(Element root, string? group = null)
|
||||
{
|
||||
var steps = new List<OnboardingStep>();
|
||||
|
||||
root.TraverseVisualTree(ve =>
|
||||
{
|
||||
if (!Onboarding.IsOnboardingStep(ve))
|
||||
return;
|
||||
|
||||
var step = OnboardingStep.FromElement(ve);
|
||||
|
||||
// Filter by group if specified
|
||||
if (group != null && !string.Equals(step.Group, group, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
steps.Add(step);
|
||||
});
|
||||
|
||||
// Sort by Order, then by visual tree position (implicit from traversal order)
|
||||
return steps
|
||||
.OrderBy(s => s.Order)
|
||||
.ThenBy(s => steps.IndexOf(s))
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a specific step by its key.
|
||||
/// </summary>
|
||||
public static OnboardingStep? FindStepByKey(Element root, string stepKey)
|
||||
{
|
||||
OnboardingStep? found = null;
|
||||
|
||||
root.TraverseVisualTree(ve =>
|
||||
{
|
||||
if (found != null)
|
||||
return;
|
||||
|
||||
var key = Onboarding.GetEffectiveStepKey(ve);
|
||||
if (string.Equals(key, stepKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = OnboardingStep.FromElement(ve);
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique group names found in the visual tree.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> GetGroups(Element root)
|
||||
{
|
||||
var groups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
root.TraverseVisualTree(ve =>
|
||||
{
|
||||
if (!Onboarding.IsOnboardingStep(ve))
|
||||
return;
|
||||
|
||||
var group = Onboarding.GetGroup(ve);
|
||||
if (!string.IsNullOrWhiteSpace(group))
|
||||
groups.Add(group);
|
||||
});
|
||||
|
||||
return groups.OrderBy(g => g).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts the total number of steps, optionally filtered by group.
|
||||
/// </summary>
|
||||
public static int CountSteps(Element root, string? group = null)
|
||||
{
|
||||
return FindSteps(root, group).Count;
|
||||
}
|
||||
}
|
||||
89
MarketAlly.MASpotlightTour/OnboardingStep.cs
Normal file
89
MarketAlly.MASpotlightTour/OnboardingStep.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single step in an onboarding tour.
|
||||
/// </summary>
|
||||
public class OnboardingStep
|
||||
{
|
||||
/// <summary>
|
||||
/// The target visual element for this step.
|
||||
/// </summary>
|
||||
public required VisualElement Target { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique key identifying this step.
|
||||
/// </summary>
|
||||
public string? StepKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Title text displayed in the callout.
|
||||
/// </summary>
|
||||
public string? Title { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Description text displayed in the callout.
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Order/sequence number within the group.
|
||||
/// </summary>
|
||||
public int Order { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Group name for organizing multiple tours.
|
||||
/// </summary>
|
||||
public string? Group { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether spotlight cutout is enabled for this step.
|
||||
/// </summary>
|
||||
public bool SpotlightEnabled { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Callout placement relative to the spotlight.
|
||||
/// </summary>
|
||||
public CalloutPlacement Placement { get; init; } = CalloutPlacement.Auto;
|
||||
|
||||
/// <summary>
|
||||
/// Shape of the spotlight cutout.
|
||||
/// </summary>
|
||||
public SpotlightShape SpotlightShape { get; init; } = SpotlightShape.RoundedRectangle;
|
||||
|
||||
/// <summary>
|
||||
/// Padding around the target in the spotlight cutout.
|
||||
/// </summary>
|
||||
public Thickness SpotlightPadding { get; init; } = new(8);
|
||||
|
||||
/// <summary>
|
||||
/// Corner radius for RoundedRectangle shape.
|
||||
/// </summary>
|
||||
public double SpotlightCornerRadius { get; init; } = 8.0;
|
||||
|
||||
/// <summary>
|
||||
/// Behavior when user taps the spotlight.
|
||||
/// </summary>
|
||||
public SpotlightTapBehavior TapBehavior { get; init; } = SpotlightTapBehavior.None;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OnboardingStep from a tagged VisualElement.
|
||||
/// </summary>
|
||||
public static OnboardingStep FromElement(VisualElement element)
|
||||
{
|
||||
return new OnboardingStep
|
||||
{
|
||||
Target = element,
|
||||
StepKey = Onboarding.GetEffectiveStepKey(element),
|
||||
Title = Onboarding.GetTitle(element),
|
||||
Description = Onboarding.GetDescription(element),
|
||||
Order = Onboarding.GetOrder(element),
|
||||
Group = Onboarding.GetGroup(element),
|
||||
SpotlightEnabled = Onboarding.GetSpotlightEnabled(element),
|
||||
Placement = Onboarding.GetPlacement(element),
|
||||
SpotlightShape = Onboarding.GetSpotlightShape(element),
|
||||
SpotlightPadding = Onboarding.GetSpotlightPadding(element),
|
||||
SpotlightCornerRadius = Onboarding.GetSpotlightCornerRadius(element),
|
||||
TapBehavior = Onboarding.GetTapBehavior(element)
|
||||
};
|
||||
}
|
||||
}
|
||||
293
MarketAlly.MASpotlightTour/SpotlightOverlay.cs
Normal file
293
MarketAlly.MASpotlightTour/SpotlightOverlay.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using Microsoft.Maui.Controls.Shapes;
|
||||
using Path = Microsoft.Maui.Controls.Shapes.Path;
|
||||
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// A visual overlay that dims the screen with a spotlight cutout.
|
||||
/// </summary>
|
||||
public class SpotlightOverlay : Grid
|
||||
{
|
||||
private readonly Path _dimPath;
|
||||
private Rect _spotlightRect;
|
||||
private SpotlightShape _spotlightShape = SpotlightShape.RoundedRectangle;
|
||||
private double _cornerRadius = 8.0;
|
||||
|
||||
/// <summary>
|
||||
/// The opacity of the dimmed area (0-1).
|
||||
/// </summary>
|
||||
public static readonly BindableProperty DimOpacityProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(DimOpacity),
|
||||
typeof(double),
|
||||
typeof(SpotlightOverlay),
|
||||
0.6,
|
||||
propertyChanged: (b, _, _) => ((SpotlightOverlay)b).UpdatePath());
|
||||
|
||||
public double DimOpacity
|
||||
{
|
||||
get => (double)GetValue(DimOpacityProperty);
|
||||
set => SetValue(DimOpacityProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The color of the dimmed overlay.
|
||||
/// </summary>
|
||||
public static readonly BindableProperty DimColorProperty =
|
||||
BindableProperty.Create(
|
||||
nameof(DimColor),
|
||||
typeof(Color),
|
||||
typeof(SpotlightOverlay),
|
||||
Colors.Black,
|
||||
propertyChanged: (b, _, _) => ((SpotlightOverlay)b).UpdatePath());
|
||||
|
||||
public Color DimColor
|
||||
{
|
||||
get => (Color)GetValue(DimColorProperty);
|
||||
set => SetValue(DimColorProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the spotlight area is tapped.
|
||||
/// </summary>
|
||||
public event EventHandler? SpotlightTapped;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when the dimmed area (outside spotlight) is tapped.
|
||||
/// </summary>
|
||||
public event EventHandler? DimmedAreaTapped;
|
||||
|
||||
public SpotlightOverlay()
|
||||
{
|
||||
BackgroundColor = Colors.Transparent;
|
||||
InputTransparent = false;
|
||||
HorizontalOptions = LayoutOptions.Fill;
|
||||
VerticalOptions = LayoutOptions.Fill;
|
||||
|
||||
_dimPath = new Path
|
||||
{
|
||||
Fill = new SolidColorBrush(DimColor.WithAlpha((float)DimOpacity)),
|
||||
InputTransparent = true,
|
||||
HorizontalOptions = LayoutOptions.Fill,
|
||||
VerticalOptions = LayoutOptions.Fill
|
||||
};
|
||||
|
||||
Children.Add(_dimPath);
|
||||
|
||||
SizeChanged += (_, _) => UpdatePath();
|
||||
|
||||
// Handle taps
|
||||
var tapGesture = new TapGestureRecognizer();
|
||||
tapGesture.Tapped += OnOverlayTapped;
|
||||
GestureRecognizers.Add(tapGesture);
|
||||
}
|
||||
|
||||
private void OnOverlayTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
var position = e.GetPosition(this);
|
||||
if (position.HasValue && _spotlightRect.Contains(position.Value))
|
||||
{
|
||||
SpotlightTapped?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
DimmedAreaTapped?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the spotlight cutout rectangle with shape options.
|
||||
/// </summary>
|
||||
public void SetSpotlight(Rect rect, SpotlightShape shape = SpotlightShape.RoundedRectangle, double cornerRadius = 8.0)
|
||||
{
|
||||
_spotlightRect = rect;
|
||||
_spotlightShape = shape;
|
||||
_cornerRadius = cornerRadius;
|
||||
UpdatePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the spotlight (full dim).
|
||||
/// </summary>
|
||||
public void ClearSpotlight()
|
||||
{
|
||||
_spotlightRect = Rect.Zero;
|
||||
UpdatePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current spotlight rectangle.
|
||||
/// </summary>
|
||||
public Rect SpotlightRect => _spotlightRect;
|
||||
|
||||
private void UpdatePath()
|
||||
{
|
||||
if (Width <= 0 || Height <= 0)
|
||||
return;
|
||||
|
||||
_dimPath.Fill = new SolidColorBrush(DimColor.WithAlpha((float)DimOpacity));
|
||||
|
||||
var geometry = new PathGeometry
|
||||
{
|
||||
FillRule = FillRule.EvenOdd
|
||||
};
|
||||
|
||||
// Outer full-screen rectangle
|
||||
var outerFigure = new PathFigure
|
||||
{
|
||||
StartPoint = new Point(0, 0),
|
||||
IsClosed = true
|
||||
};
|
||||
outerFigure.Segments.Add(new LineSegment(new Point(Width, 0)));
|
||||
outerFigure.Segments.Add(new LineSegment(new Point(Width, Height)));
|
||||
outerFigure.Segments.Add(new LineSegment(new Point(0, Height)));
|
||||
outerFigure.Segments.Add(new LineSegment(new Point(0, 0)));
|
||||
geometry.Figures.Add(outerFigure);
|
||||
|
||||
// Inner spotlight hole (if valid)
|
||||
if (_spotlightRect.Width > 0 && _spotlightRect.Height > 0)
|
||||
{
|
||||
var innerFigure = CreateSpotlightFigure();
|
||||
geometry.Figures.Add(innerFigure);
|
||||
}
|
||||
|
||||
_dimPath.Data = geometry;
|
||||
}
|
||||
|
||||
private PathFigure CreateSpotlightFigure()
|
||||
{
|
||||
return _spotlightShape switch
|
||||
{
|
||||
SpotlightShape.Rectangle => CreateRectangleFigure(),
|
||||
SpotlightShape.RoundedRectangle => CreateRoundedRectangleFigure(),
|
||||
SpotlightShape.Circle => CreateCircleFigure(),
|
||||
_ => CreateRoundedRectangleFigure()
|
||||
};
|
||||
}
|
||||
|
||||
private PathFigure CreateRectangleFigure()
|
||||
{
|
||||
var figure = new PathFigure
|
||||
{
|
||||
StartPoint = new Point(_spotlightRect.Left, _spotlightRect.Top),
|
||||
IsClosed = true
|
||||
};
|
||||
figure.Segments.Add(new LineSegment(new Point(_spotlightRect.Right, _spotlightRect.Top)));
|
||||
figure.Segments.Add(new LineSegment(new Point(_spotlightRect.Right, _spotlightRect.Bottom)));
|
||||
figure.Segments.Add(new LineSegment(new Point(_spotlightRect.Left, _spotlightRect.Bottom)));
|
||||
figure.Segments.Add(new LineSegment(new Point(_spotlightRect.Left, _spotlightRect.Top)));
|
||||
return figure;
|
||||
}
|
||||
|
||||
private PathFigure CreateRoundedRectangleFigure()
|
||||
{
|
||||
var r = Math.Min(_cornerRadius, Math.Min(_spotlightRect.Width / 2, _spotlightRect.Height / 2));
|
||||
var rect = _spotlightRect;
|
||||
|
||||
var figure = new PathFigure
|
||||
{
|
||||
StartPoint = new Point(rect.Left + r, rect.Top),
|
||||
IsClosed = true
|
||||
};
|
||||
|
||||
// Top edge
|
||||
figure.Segments.Add(new LineSegment(new Point(rect.Right - r, rect.Top)));
|
||||
// Top-right corner
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(rect.Right, rect.Top + r),
|
||||
Size = new Size(r, r),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
// Right edge
|
||||
figure.Segments.Add(new LineSegment(new Point(rect.Right, rect.Bottom - r)));
|
||||
// Bottom-right corner
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(rect.Right - r, rect.Bottom),
|
||||
Size = new Size(r, r),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
// Bottom edge
|
||||
figure.Segments.Add(new LineSegment(new Point(rect.Left + r, rect.Bottom)));
|
||||
// Bottom-left corner
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(rect.Left, rect.Bottom - r),
|
||||
Size = new Size(r, r),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
// Left edge
|
||||
figure.Segments.Add(new LineSegment(new Point(rect.Left, rect.Top + r)));
|
||||
// Top-left corner
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(rect.Left + r, rect.Top),
|
||||
Size = new Size(r, r),
|
||||
SweepDirection = SweepDirection.Clockwise
|
||||
});
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
private PathFigure CreateCircleFigure()
|
||||
{
|
||||
var center = new Point(_spotlightRect.Center.X, _spotlightRect.Center.Y);
|
||||
// Use the diagonal to create a circle that encompasses the rectangle
|
||||
var radius = Math.Sqrt(_spotlightRect.Width * _spotlightRect.Width + _spotlightRect.Height * _spotlightRect.Height) / 2;
|
||||
// But cap it to not be excessively large
|
||||
radius = Math.Min(radius, Math.Max(_spotlightRect.Width, _spotlightRect.Height) * 0.75);
|
||||
|
||||
var figure = new PathFigure
|
||||
{
|
||||
StartPoint = new Point(center.X - radius, center.Y),
|
||||
IsClosed = true
|
||||
};
|
||||
|
||||
// Upper half
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(center.X + radius, center.Y),
|
||||
Size = new Size(radius, radius),
|
||||
SweepDirection = SweepDirection.Clockwise,
|
||||
IsLargeArc = true
|
||||
});
|
||||
|
||||
// Lower half
|
||||
figure.Segments.Add(new ArcSegment
|
||||
{
|
||||
Point = new Point(center.X - radius, center.Y),
|
||||
Size = new Size(radius, radius),
|
||||
SweepDirection = SweepDirection.Clockwise,
|
||||
IsLargeArc = true
|
||||
});
|
||||
|
||||
return figure;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates the spotlight to a new position.
|
||||
/// </summary>
|
||||
public async Task AnimateToAsync(Rect targetRect, SpotlightShape shape, double cornerRadius, uint duration = 250)
|
||||
{
|
||||
var startRect = _spotlightRect;
|
||||
_spotlightShape = shape;
|
||||
_cornerRadius = cornerRadius;
|
||||
|
||||
var animation = new Animation(v =>
|
||||
{
|
||||
_spotlightRect = new Rect(
|
||||
startRect.X + (targetRect.X - startRect.X) * v,
|
||||
startRect.Y + (targetRect.Y - startRect.Y) * v,
|
||||
startRect.Width + (targetRect.Width - startRect.Width) * v,
|
||||
startRect.Height + (targetRect.Height - startRect.Height) * v);
|
||||
UpdatePath();
|
||||
});
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
animation.Commit(this, "SpotlightAnimation", length: duration, easing: Easing.CubicInOut,
|
||||
finished: (_, _) => tcs.SetResult(true));
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
137
MarketAlly.MASpotlightTour/VisualElementExtensions.cs
Normal file
137
MarketAlly.MASpotlightTour/VisualElementExtensions.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
namespace MarketAlly.MASpotlightTour;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for VisualElement bounds calculations.
|
||||
/// </summary>
|
||||
public static class VisualElementExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of a view relative to a root element.
|
||||
/// </summary>
|
||||
/// <param name="view">The target view.</param>
|
||||
/// <param name="root">The root element to calculate bounds relative to.</param>
|
||||
/// <returns>The bounds rectangle relative to the root.</returns>
|
||||
public static Rect GetAbsoluteBoundsRelativeTo(this VisualElement view, VisualElement root)
|
||||
{
|
||||
if (view == null || root == null)
|
||||
return Rect.Zero;
|
||||
|
||||
double x = view.X;
|
||||
double y = view.Y;
|
||||
double width = view.Width;
|
||||
double height = view.Height;
|
||||
|
||||
Element? parent = view.Parent;
|
||||
while (parent is VisualElement ve && ve != root)
|
||||
{
|
||||
x += ve.X;
|
||||
y += ve.Y;
|
||||
|
||||
// Account for ScrollView scroll position
|
||||
if (ve is ScrollView scrollView)
|
||||
{
|
||||
x -= scrollView.ScrollX;
|
||||
y -= scrollView.ScrollY;
|
||||
}
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
return new Rect(x, y, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds with padding applied.
|
||||
/// </summary>
|
||||
public static Rect GetAbsoluteBoundsWithPadding(this VisualElement view, VisualElement root, Thickness padding)
|
||||
{
|
||||
var bounds = view.GetAbsoluteBoundsRelativeTo(root);
|
||||
|
||||
return new Rect(
|
||||
bounds.X - padding.Left,
|
||||
bounds.Y - padding.Top,
|
||||
bounds.Width + padding.Left + padding.Right,
|
||||
bounds.Height + padding.Top + padding.Bottom);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the element is currently visible within the viewport.
|
||||
/// </summary>
|
||||
public static bool IsVisibleInViewport(this VisualElement view, VisualElement root)
|
||||
{
|
||||
var bounds = view.GetAbsoluteBoundsRelativeTo(root);
|
||||
var rootBounds = new Rect(0, 0, root.Width, root.Height);
|
||||
|
||||
return bounds.IntersectsWith(rootBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the nearest ScrollView ancestor.
|
||||
/// </summary>
|
||||
public static ScrollView? FindAncestorScrollView(this VisualElement view)
|
||||
{
|
||||
Element? parent = view.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent is ScrollView scrollView)
|
||||
return scrollView;
|
||||
parent = parent.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the element is visible by scrolling if necessary.
|
||||
/// </summary>
|
||||
public static async Task EnsureVisibleAsync(this VisualElement view, bool animate = true)
|
||||
{
|
||||
var scrollView = view.FindAncestorScrollView();
|
||||
if (scrollView != null)
|
||||
{
|
||||
await scrollView.ScrollToAsync(view, ScrollToPosition.MakeVisible, animate);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center point of the element relative to root.
|
||||
/// </summary>
|
||||
public static Point GetCenterRelativeTo(this VisualElement view, VisualElement root)
|
||||
{
|
||||
var bounds = view.GetAbsoluteBoundsRelativeTo(root);
|
||||
return new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverses the visual tree and invokes an action on each VisualElement.
|
||||
/// </summary>
|
||||
public static void TraverseVisualTree(this Element root, Action<VisualElement> action)
|
||||
{
|
||||
if (root is VisualElement ve)
|
||||
action(ve);
|
||||
|
||||
if (root is IVisualTreeElement vte)
|
||||
{
|
||||
foreach (var child in vte.GetVisualChildren())
|
||||
{
|
||||
if (child is Element element)
|
||||
TraverseVisualTree(element, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all VisualElements matching a predicate.
|
||||
/// </summary>
|
||||
public static IEnumerable<VisualElement> FindAll(this Element root, Func<VisualElement, bool> predicate)
|
||||
{
|
||||
var results = new List<VisualElement>();
|
||||
|
||||
root.TraverseVisualTree(ve =>
|
||||
{
|
||||
if (predicate(ve))
|
||||
results.Add(ve);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
14
Test.SpotlightTour/App.xaml
Normal file
14
Test.SpotlightTour/App.xaml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Test.SpotlightTour"
|
||||
x:Class="Test.SpotlightTour.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
22
Test.SpotlightTour/App.xaml.cs
Normal file
22
Test.SpotlightTour/App.xaml.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
var window = new Window(new AppShell());
|
||||
|
||||
#if WINDOWS
|
||||
window.Width = 700;
|
||||
window.Height = 900;
|
||||
#endif
|
||||
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Test.SpotlightTour/AppShell.xaml
Normal file
66
Test.SpotlightTour/AppShell.xaml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Shell
|
||||
x:Class="Test.SpotlightTour.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Test.SpotlightTour"
|
||||
xmlns:pages="clr-namespace:Test.SpotlightTour.Pages"
|
||||
Title="MASpotlightTour Demo">
|
||||
|
||||
<FlyoutItem Title="Home" Icon="{OnPlatform Default=icon_home.png}">
|
||||
<ShellContent
|
||||
Title="Home"
|
||||
ContentTemplate="{DataTemplate local:MainPage}"
|
||||
Route="MainPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Basic Tour" Icon="{OnPlatform Default=icon_tour.png}">
|
||||
<ShellContent
|
||||
Title="Basic Tour"
|
||||
ContentTemplate="{DataTemplate pages:BasicTourPage}"
|
||||
Route="BasicTourPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Spotlight Shapes" Icon="{OnPlatform Default=icon_shapes.png}">
|
||||
<ShellContent
|
||||
Title="Spotlight Shapes"
|
||||
ContentTemplate="{DataTemplate pages:ShapesPage}"
|
||||
Route="ShapesPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Auto Features" Icon="{OnPlatform Default=icon_auto.png}">
|
||||
<ShellContent
|
||||
Title="Auto Features"
|
||||
ContentTemplate="{DataTemplate pages:AutoStartPage}"
|
||||
Route="AutoStartPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Scroll Support" Icon="{OnPlatform Default=icon_scroll.png}">
|
||||
<ShellContent
|
||||
Title="Scroll Support"
|
||||
ContentTemplate="{DataTemplate pages:ScrollDemoPage}"
|
||||
Route="ScrollDemoPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Display Modes" Icon="{OnPlatform Default=icon_modes.png}">
|
||||
<ShellContent
|
||||
Title="Display Modes"
|
||||
ContentTemplate="{DataTemplate pages:DisplayModesPage}"
|
||||
Route="DisplayModesPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Positioning Modes" Icon="{OnPlatform Default=icon_position.png}">
|
||||
<ShellContent
|
||||
Title="Positioning Modes"
|
||||
ContentTemplate="{DataTemplate pages:PositioningModesPage}"
|
||||
Route="PositioningModesPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Themes" Icon="{OnPlatform Default=icon_theme.png}">
|
||||
<ShellContent
|
||||
Title="Themes"
|
||||
ContentTemplate="{DataTemplate pages:ThemesPage}"
|
||||
Route="ThemesPage" />
|
||||
</FlyoutItem>
|
||||
|
||||
</Shell>
|
||||
10
Test.SpotlightTour/AppShell.xaml.cs
Normal file
10
Test.SpotlightTour/AppShell.xaml.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
public partial class AppShell : Shell
|
||||
{
|
||||
public AppShell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Test.SpotlightTour/GlobalXmlns.cs
Normal file
2
Test.SpotlightTour/GlobalXmlns.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "Test.SpotlightTour")]
|
||||
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/2021/maui", "Test.SpotlightTour.Pages")]
|
||||
107
Test.SpotlightTour/MainPage.xaml
Normal file
107
Test.SpotlightTour/MainPage.xaml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Test.SpotlightTour.MainPage"
|
||||
Title="MASpotlightTour Demo">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Image
|
||||
Source="dotnet_bot.png"
|
||||
HeightRequest="150"
|
||||
Aspect="AspectFit" />
|
||||
|
||||
<Label
|
||||
Text="MASpotlightTour"
|
||||
FontSize="28"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center" />
|
||||
|
||||
<Label
|
||||
Text="A XAML-driven onboarding and coach-marks system for .NET MAUI"
|
||||
FontSize="16"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextColor="Gray" />
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<Label
|
||||
Text="Select a demo:"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Frame BackgroundColor="{StaticResource Primary}" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnBasicTourTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Basic Tour" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Simple spotlight tour with manual trigger button" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#9C27B0" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnShapesTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Spotlight Shapes" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Rectangle, RoundedRectangle, and Circle shapes" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#FF9800" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnAutoStartTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Auto Features" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Auto-start and auto-advance for hands-free demos" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#2196F3" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnScrollDemoTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Scroll Support" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Auto-scroll to off-screen elements" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#607D8B" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnDisplayModesTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Display Modes" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="SpotlightWithCallout, CalloutOnly, SpotlightWithInlineLabel" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#00BCD4" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnPositioningModesTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Positioning Modes" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Following, FixedCorner, and AutoCorner callout positioning" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#795548" Padding="15" CornerRadius="8">
|
||||
<Frame.GestureRecognizers>
|
||||
<TapGestureRecognizer Tapped="OnThemesTapped" />
|
||||
</Frame.GestureRecognizers>
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="Themes" FontAttributes="Bold" TextColor="White" />
|
||||
<Label Text="Light, Dark, and System theme support" TextColor="White" FontSize="12" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
30
Test.SpotlightTour/MainPage.xaml.cs
Normal file
30
Test.SpotlightTour/MainPage.xaml.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Test.SpotlightTour;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnBasicTourTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//BasicTourPage");
|
||||
|
||||
private async void OnShapesTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//ShapesPage");
|
||||
|
||||
private async void OnAutoStartTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//AutoStartPage");
|
||||
|
||||
private async void OnScrollDemoTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//ScrollDemoPage");
|
||||
|
||||
private async void OnDisplayModesTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//DisplayModesPage");
|
||||
|
||||
private async void OnPositioningModesTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//PositioningModesPage");
|
||||
|
||||
private async void OnThemesTapped(object? sender, EventArgs e)
|
||||
=> await Shell.Current.GoToAsync("//ThemesPage");
|
||||
}
|
||||
25
Test.SpotlightTour/MauiProgram.cs
Normal file
25
Test.SpotlightTour/MauiProgram.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.ConfigureFonts(fonts =>
|
||||
{
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
});
|
||||
|
||||
#if DEBUG
|
||||
builder.Logging.AddDebug();
|
||||
#endif
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
105
Test.SpotlightTour/Pages/AutoStartPage.xaml
Normal file
105
Test.SpotlightTour/Pages/AutoStartPage.xaml
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.AutoStartPage"
|
||||
Title="Auto Features">
|
||||
|
||||
<Grid>
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Label
|
||||
Text="Auto Features Demo"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Label
|
||||
Text="Automatic tour start and step advancement for hands-free demos."
|
||||
TextColor="Gray" />
|
||||
|
||||
<!-- Manual Tour Button -->
|
||||
<Frame BackgroundColor="#4CAF50" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Manual Tour" FontAttributes="Bold" TextColor="White" FontSize="16" />
|
||||
<Label Text="Standard tour with manual navigation." TextColor="White" FontSize="13" />
|
||||
<Button Text="Start Manual Tour"
|
||||
BackgroundColor="White"
|
||||
TextColor="#4CAF50"
|
||||
Clicked="OnManualTourClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Auto-Advance Tour Button -->
|
||||
<Frame BackgroundColor="#2196F3" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Auto-Advance Tour" FontAttributes="Bold" TextColor="White" FontSize="16" />
|
||||
<Label Text="Each step advances automatically after 3 seconds." TextColor="White" FontSize="13" />
|
||||
<Button Text="Start Auto-Advance Tour"
|
||||
BackgroundColor="White"
|
||||
TextColor="#2196F3"
|
||||
Clicked="OnAutoAdvanceTourClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="10" Color="Transparent" />
|
||||
|
||||
<!-- Sample elements to highlight -->
|
||||
<Label Text="Sample Elements:" FontSize="18" FontAttributes="Bold" />
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#E8F5E9"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.StepKey="step1"
|
||||
onboard:Onboarding.Title="First Feature"
|
||||
onboard:Onboarding.Description="This is the first feature in your app. In auto-advance mode, the tour will move to the next step automatically."
|
||||
onboard:Onboarding.Order="1">
|
||||
<Label Text="Feature 1" FontAttributes="Bold" HorizontalOptions="Center" TextColor="#2E7D32" />
|
||||
</Frame>
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#E3F2FD"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.StepKey="step2"
|
||||
onboard:Onboarding.Title="Second Feature"
|
||||
onboard:Onboarding.Description="The second feature is highlighted here. Watch the timer - it will advance automatically!"
|
||||
onboard:Onboarding.Order="2">
|
||||
<Label Text="Feature 2" FontAttributes="Bold" HorizontalOptions="Center" TextColor="#1565C0" />
|
||||
</Frame>
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#FFF3E0"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.StepKey="step3"
|
||||
onboard:Onboarding.Title="Third Feature"
|
||||
onboard:Onboarding.Description="Almost done! This is the third feature. One more to go."
|
||||
onboard:Onboarding.Order="3">
|
||||
<Label Text="Feature 3" FontAttributes="Bold" HorizontalOptions="Center" TextColor="#E65100" />
|
||||
</Frame>
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#FCE4EC"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.StepKey="step4"
|
||||
onboard:Onboarding.Title="Final Feature"
|
||||
onboard:Onboarding.Description="This is the last step. The tour will complete automatically or you can press Done."
|
||||
onboard:Onboarding.Order="4">
|
||||
<Label Text="Feature 4" FontAttributes="Bold" HorizontalOptions="Center" TextColor="#AD1457" />
|
||||
</Frame>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<onboard:OnboardingHost
|
||||
x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
CornerNavigatorPlacement="BottomRight" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
21
Test.SpotlightTour/Pages/AutoStartPage.xaml.cs
Normal file
21
Test.SpotlightTour/Pages/AutoStartPage.xaml.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class AutoStartPage : ContentPage
|
||||
{
|
||||
public AutoStartPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnManualTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.AutoAdvanceDelay = 0; // Disable auto-advance
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnAutoAdvanceTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.AutoAdvanceDelay = 3000; // 3 seconds per step
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
65
Test.SpotlightTour/Pages/BasicTourPage.xaml
Normal file
65
Test.SpotlightTour/Pages/BasicTourPage.xaml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.BasicTourPage"
|
||||
Title="Basic Tour">
|
||||
|
||||
<Grid>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Label
|
||||
Text="Basic Tour Demo"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
onboard:Onboarding.Title="Welcome!"
|
||||
onboard:Onboarding.Description="This demo shows a simple spotlight tour triggered by a button."
|
||||
onboard:Onboarding.Order="1" />
|
||||
|
||||
<Label
|
||||
Text="Tap the button below to start the tour. Each element with onboarding properties will be highlighted."
|
||||
TextColor="Gray" />
|
||||
|
||||
<Button
|
||||
x:Name="StartTourBtn"
|
||||
Text="Start Tour"
|
||||
Clicked="OnStartTourClicked"
|
||||
BackgroundColor="{StaticResource Primary}"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Start Button"
|
||||
onboard:Onboarding.Description="This button triggers the spotlight tour manually."
|
||||
onboard:Onboarding.Order="2" />
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#E3F2FD"
|
||||
Padding="15"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.Title="Info Card"
|
||||
onboard:Onboarding.Description="Cards can also be highlighted to explain UI sections."
|
||||
onboard:Onboarding.Order="3">
|
||||
<Label Text="This is an info card that will be highlighted during the tour." />
|
||||
</Frame>
|
||||
|
||||
<HorizontalStackLayout Spacing="10" HorizontalOptions="Center">
|
||||
<Button
|
||||
Text="Action 1"
|
||||
BackgroundColor="#4CAF50"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Action Button"
|
||||
onboard:Onboarding.Description="Individual buttons in a row can be highlighted. This is the last step!"
|
||||
onboard:Onboarding.Order="4" />
|
||||
|
||||
<Button Text="Action 2" BackgroundColor="Gray" TextColor="White" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Onboarding Host -->
|
||||
<onboard:OnboardingHost
|
||||
x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
ShowSkipButton="True"
|
||||
DimOpacity="0.7" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
14
Test.SpotlightTour/Pages/BasicTourPage.xaml.cs
Normal file
14
Test.SpotlightTour/Pages/BasicTourPage.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class BasicTourPage : ContentPage
|
||||
{
|
||||
public BasicTourPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnStartTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
87
Test.SpotlightTour/Pages/DisplayModesPage.xaml
Normal file
87
Test.SpotlightTour/Pages/DisplayModesPage.xaml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.DisplayModesPage"
|
||||
Title="Display Modes">
|
||||
|
||||
<Grid>
|
||||
<VerticalStackLayout Padding="20" Spacing="15">
|
||||
|
||||
<Label
|
||||
Text="Tour Display Modes"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
onboard:Onboarding.Title="Display Modes"
|
||||
onboard:Onboarding.Description="Three different ways to present your onboarding tour."
|
||||
onboard:Onboarding.Order="1" />
|
||||
|
||||
<Label Text="Choose a mode to see how it looks:" TextColor="Gray" />
|
||||
|
||||
<!-- Mode selection buttons -->
|
||||
<Button
|
||||
x:Name="SpotlightCalloutBtn"
|
||||
Text="Spotlight + Callout Card"
|
||||
Clicked="OnSpotlightCalloutClicked"
|
||||
BackgroundColor="{StaticResource Primary}"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Full Experience"
|
||||
onboard:Onboarding.Description="Dim overlay + spotlight cutout + callout card with nav buttons. Best for focused tutorials."
|
||||
onboard:Onboarding.Order="2" />
|
||||
|
||||
<Button
|
||||
x:Name="CalloutOnlyBtn"
|
||||
Text="Callout Only (No Dimming)"
|
||||
Clicked="OnCalloutOnlyClicked"
|
||||
BackgroundColor="#9C27B0"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Light Touch"
|
||||
onboard:Onboarding.Description="Floating callouts without dimming. Users can still interact with the app. Great for subtle hints."
|
||||
onboard:Onboarding.Order="3" />
|
||||
|
||||
<Button
|
||||
x:Name="InlineLabelBtn"
|
||||
Text="Spotlight + Inline Labels"
|
||||
Clicked="OnInlineLabelClicked"
|
||||
BackgroundColor="#FF9800"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Minimal UI"
|
||||
onboard:Onboarding.Description="Spotlight with simple labels. Use corner navigator for nav. Clean and minimal."
|
||||
onboard:Onboarding.Order="4" />
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<!-- Sample elements to highlight -->
|
||||
<Frame
|
||||
BackgroundColor="#E8F5E9"
|
||||
Padding="15"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.Title="Sample Card"
|
||||
onboard:Onboarding.Description="This card demonstrates how each mode highlights elements differently."
|
||||
onboard:Onboarding.Order="5">
|
||||
<Label Text="Sample content card for the tour demo." />
|
||||
</Frame>
|
||||
|
||||
<HorizontalStackLayout Spacing="10" HorizontalOptions="Center">
|
||||
<Button
|
||||
Text="Save"
|
||||
BackgroundColor="#4CAF50"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Final Step"
|
||||
onboard:Onboarding.Description="Tour complete! Try a different mode to compare."
|
||||
onboard:Onboarding.Order="6" />
|
||||
<Button Text="Cancel" BackgroundColor="Gray" TextColor="White" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Onboarding Host - DisplayMode set in code -->
|
||||
<onboard:OnboardingHost
|
||||
x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
CornerNavigatorPlacement="TopRight"
|
||||
ShowArrow="True"
|
||||
DimOpacity="0.7" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
29
Test.SpotlightTour/Pages/DisplayModesPage.xaml.cs
Normal file
29
Test.SpotlightTour/Pages/DisplayModesPage.xaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MarketAlly.MASpotlightTour;
|
||||
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class DisplayModesPage : ContentPage
|
||||
{
|
||||
public DisplayModesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnSpotlightCalloutClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnCalloutOnlyClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.CalloutOnly;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnInlineLabelClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithInlineLabel;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
137
Test.SpotlightTour/Pages/PositioningModesPage.xaml
Normal file
137
Test.SpotlightTour/Pages/PositioningModesPage.xaml
Normal file
@@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:spot="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.PositioningModesPage"
|
||||
Title="Callout Positioning Modes">
|
||||
|
||||
<Grid>
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Label Text="Callout Positioning Modes"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center" />
|
||||
|
||||
<Label Text="Choose how the callout dialog is positioned relative to highlighted elements."
|
||||
FontSize="14"
|
||||
TextColor="Gray"
|
||||
HorizontalTextAlignment="Center" />
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<!-- Following Mode -->
|
||||
<Frame BackgroundColor="#4CAF50" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Following Mode (Default)" FontAttributes="Bold" TextColor="White" FontSize="16" />
|
||||
<Label Text="Callout positions itself near the highlighted element (top, bottom, left, or right based on available space)."
|
||||
TextColor="White" FontSize="13" />
|
||||
<Button Text="Try Following Mode"
|
||||
BackgroundColor="White"
|
||||
TextColor="#4CAF50"
|
||||
Clicked="OnFollowingModeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Fixed Corner Mode -->
|
||||
<Frame BackgroundColor="#2196F3" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Fixed Corner Mode" FontAttributes="Bold" TextColor="White" FontSize="16" />
|
||||
<Label Text="Callout always appears in a specific screen corner (Bottom-Left in this demo)."
|
||||
TextColor="White" FontSize="13" />
|
||||
<Button Text="Try Fixed Corner Mode"
|
||||
BackgroundColor="White"
|
||||
TextColor="#2196F3"
|
||||
Clicked="OnFixedCornerModeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Auto Corner Mode -->
|
||||
<Frame BackgroundColor="#9C27B0" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Auto Corner Mode" FontAttributes="Bold" TextColor="White" FontSize="16" />
|
||||
<Label Text="Callout automatically picks the corner that won't interfere with the highlighted element."
|
||||
TextColor="White" FontSize="13" />
|
||||
<Button Text="Try Auto Corner Mode"
|
||||
BackgroundColor="White"
|
||||
TextColor="#9C27B0"
|
||||
Clicked="OnAutoCornerModeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<!-- Sample elements to highlight -->
|
||||
<Label Text="Sample Elements to Highlight:"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto" ColumnSpacing="15" RowSpacing="15">
|
||||
<Frame Grid.Row="0" Grid.Column="0"
|
||||
BackgroundColor="#E3F2FD"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
spot:Onboarding.StepKey="topLeft"
|
||||
spot:Onboarding.Title="Top Left Element"
|
||||
spot:Onboarding.Description="This element is in the top-left area. Watch how the callout positions itself."
|
||||
spot:Onboarding.Order="1">
|
||||
<Label Text="Top Left" HorizontalOptions="Center" TextColor="#1976D2" FontAttributes="Bold" />
|
||||
</Frame>
|
||||
|
||||
<Frame Grid.Row="0" Grid.Column="1"
|
||||
BackgroundColor="#E8F5E9"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
spot:Onboarding.StepKey="topRight"
|
||||
spot:Onboarding.Title="Top Right Element"
|
||||
spot:Onboarding.Description="This element is in the top-right area. The callout adapts its position."
|
||||
spot:Onboarding.Order="2">
|
||||
<Label Text="Top Right" HorizontalOptions="Center" TextColor="#388E3C" FontAttributes="Bold" />
|
||||
</Frame>
|
||||
|
||||
<Frame Grid.Row="1" Grid.Column="0"
|
||||
BackgroundColor="#FFF3E0"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
spot:Onboarding.StepKey="bottomLeft"
|
||||
spot:Onboarding.Title="Bottom Left Element"
|
||||
spot:Onboarding.Description="This element is in the bottom-left area. Notice how positioning changes."
|
||||
spot:Onboarding.Order="3">
|
||||
<Label Text="Bottom Left" HorizontalOptions="Center" TextColor="#F57C00" FontAttributes="Bold" />
|
||||
</Frame>
|
||||
|
||||
<Frame Grid.Row="1" Grid.Column="1"
|
||||
BackgroundColor="#FCE4EC"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
spot:Onboarding.StepKey="bottomRight"
|
||||
spot:Onboarding.Title="Bottom Right Element"
|
||||
spot:Onboarding.Description="This element is in the bottom-right area. Each mode handles this differently."
|
||||
spot:Onboarding.Order="4">
|
||||
<Label Text="Bottom Right" HorizontalOptions="Center" TextColor="#C2185B" FontAttributes="Bold" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
|
||||
<Frame BackgroundColor="#F5F5F5"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
spot:Onboarding.StepKey="center"
|
||||
spot:Onboarding.Title="Center Element"
|
||||
spot:Onboarding.Description="A center element demonstrates how auto-corner mode picks the best position."
|
||||
spot:Onboarding.Order="5">
|
||||
<Label Text="Center Element" HorizontalOptions="Center" TextColor="#616161" FontAttributes="Bold" />
|
||||
</Frame>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<spot:OnboardingHost x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
CornerNavigatorPlacement="BottomRight" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
33
Test.SpotlightTour/Pages/PositioningModesPage.xaml.cs
Normal file
33
Test.SpotlightTour/Pages/PositioningModesPage.xaml.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using MarketAlly.MASpotlightTour;
|
||||
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class PositioningModesPage : ContentPage
|
||||
{
|
||||
public PositioningModesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnFollowingModeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.CalloutPositionMode = CalloutPositionMode.Following;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnFixedCornerModeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.CalloutPositionMode = CalloutPositionMode.FixedCorner;
|
||||
OnboardingHost.CalloutCorner = CalloutCorner.BottomLeft;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnAutoCornerModeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.CalloutPositionMode = CalloutPositionMode.AutoCorner;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
86
Test.SpotlightTour/Pages/ScrollDemoPage.xaml
Normal file
86
Test.SpotlightTour/Pages/ScrollDemoPage.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.ScrollDemoPage"
|
||||
Title="Scroll Support">
|
||||
|
||||
<Grid>
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Label
|
||||
Text="Scroll Support Demo"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
onboard:Onboarding.Title="Scroll Demo"
|
||||
onboard:Onboarding.Description="This tour demonstrates auto-scrolling to off-screen elements."
|
||||
onboard:Onboarding.Order="1" />
|
||||
|
||||
<Button
|
||||
x:Name="StartTourBtn"
|
||||
Text="Start Scroll Tour"
|
||||
Clicked="OnStartTourClicked"
|
||||
BackgroundColor="{StaticResource Primary}"
|
||||
TextColor="White"
|
||||
onboard:Onboarding.Title="Start Here"
|
||||
onboard:Onboarding.Description="Click Next to see auto-scroll in action."
|
||||
onboard:Onboarding.Order="2" />
|
||||
|
||||
<Frame BackgroundColor="#E8F5E9" Padding="15" CornerRadius="8">
|
||||
<Label Text="This element is visible at the top of the page." />
|
||||
</Frame>
|
||||
|
||||
<!-- Spacer to push content below fold -->
|
||||
<BoxView HeightRequest="400" Color="Transparent" />
|
||||
|
||||
<Label
|
||||
Text="Scroll down to see more..."
|
||||
HorizontalOptions="Center"
|
||||
TextColor="Gray" />
|
||||
|
||||
<BoxView HeightRequest="400" Color="Transparent" />
|
||||
|
||||
<!-- Element below the fold -->
|
||||
<Frame
|
||||
BackgroundColor="#E3F2FD"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.Title="Below the Fold"
|
||||
onboard:Onboarding.Description="The tour automatically scrolled to show this element!"
|
||||
onboard:Onboarding.Order="3"
|
||||
onboard:Onboarding.Placement="Top">
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<Label Text="Hidden Element" FontAttributes="Bold" FontSize="18" />
|
||||
<Label Text="This element was below the visible area. The OnboardingHost automatically scrolled to bring it into view." />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="200" Color="Transparent" />
|
||||
|
||||
<Frame
|
||||
BackgroundColor="#FCE4EC"
|
||||
Padding="20"
|
||||
CornerRadius="8"
|
||||
onboard:Onboarding.Title="Even Further Down"
|
||||
onboard:Onboarding.Description="Scrolling works for any element in a ScrollView. Tour complete!"
|
||||
onboard:Onboarding.Order="4"
|
||||
onboard:Onboarding.Placement="Top">
|
||||
<Label Text="Another hidden element at the bottom." HorizontalOptions="Center" />
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="100" Color="Transparent" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- Onboarding Host -->
|
||||
<onboard:OnboardingHost
|
||||
x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
ShowArrow="True"
|
||||
DimOpacity="0.7"
|
||||
AnimationsEnabled="True" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
14
Test.SpotlightTour/Pages/ScrollDemoPage.xaml.cs
Normal file
14
Test.SpotlightTour/Pages/ScrollDemoPage.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class ScrollDemoPage : ContentPage
|
||||
{
|
||||
public ScrollDemoPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnStartTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
79
Test.SpotlightTour/Pages/ShapesPage.xaml
Normal file
79
Test.SpotlightTour/Pages/ShapesPage.xaml
Normal file
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:onboard="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.ShapesPage"
|
||||
Title="Spotlight Shapes">
|
||||
|
||||
<Grid>
|
||||
<VerticalStackLayout Padding="20" Spacing="25">
|
||||
|
||||
<Label
|
||||
Text="Spotlight Shapes"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Button
|
||||
x:Name="StartTourBtn"
|
||||
Text="Start Shapes Tour"
|
||||
Clicked="OnStartTourClicked"
|
||||
BackgroundColor="{StaticResource Primary}"
|
||||
TextColor="White" />
|
||||
|
||||
<!-- Rectangle Shape -->
|
||||
<Frame
|
||||
BackgroundColor="#FFCDD2"
|
||||
Padding="20"
|
||||
CornerRadius="0"
|
||||
onboard:Onboarding.Title="Rectangle Shape"
|
||||
onboard:Onboarding.Description="SpotlightShape='Rectangle' creates a sharp-cornered cutout."
|
||||
onboard:Onboarding.SpotlightShape="Rectangle"
|
||||
onboard:Onboarding.Order="1">
|
||||
<Label Text="Rectangle Spotlight" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
</Frame>
|
||||
|
||||
<!-- RoundedRectangle Shape -->
|
||||
<Frame
|
||||
BackgroundColor="#C8E6C9"
|
||||
Padding="20"
|
||||
CornerRadius="12"
|
||||
onboard:Onboarding.Title="Rounded Rectangle"
|
||||
onboard:Onboarding.Description="SpotlightShape='RoundedRectangle' with SpotlightCornerRadius='12' for smooth corners."
|
||||
onboard:Onboarding.SpotlightShape="RoundedRectangle"
|
||||
onboard:Onboarding.SpotlightCornerRadius="12"
|
||||
onboard:Onboarding.Order="2">
|
||||
<Label Text="Rounded Rectangle Spotlight" FontAttributes="Bold" HorizontalOptions="Center" />
|
||||
</Frame>
|
||||
|
||||
<!-- Circle Shape -->
|
||||
<Grid
|
||||
HeightRequest="120"
|
||||
WidthRequest="120"
|
||||
HorizontalOptions="Center"
|
||||
onboard:Onboarding.Title="Circle Shape"
|
||||
onboard:Onboarding.Description="SpotlightShape='Circle' creates a circular cutout around the element."
|
||||
onboard:Onboarding.SpotlightShape="Circle"
|
||||
onboard:Onboarding.SpotlightPadding="20"
|
||||
onboard:Onboarding.Order="3">
|
||||
<Ellipse
|
||||
Fill="#BBDEFB"
|
||||
WidthRequest="120"
|
||||
HeightRequest="120" />
|
||||
<Label
|
||||
Text="Circle"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center" />
|
||||
</Grid>
|
||||
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Onboarding Host -->
|
||||
<onboard:OnboardingHost
|
||||
x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
ShowArrow="True"
|
||||
DimOpacity="0.75" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
14
Test.SpotlightTour/Pages/ShapesPage.xaml.cs
Normal file
14
Test.SpotlightTour/Pages/ShapesPage.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class ShapesPage : ContentPage
|
||||
{
|
||||
public ShapesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnStartTourClicked(object? sender, EventArgs e)
|
||||
{
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
151
Test.SpotlightTour/Pages/ThemesPage.xaml
Normal file
151
Test.SpotlightTour/Pages/ThemesPage.xaml
Normal file
@@ -0,0 +1,151 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:spot="clr-namespace:MarketAlly.MASpotlightTour;assembly=MarketAlly.MASpotlightTour"
|
||||
x:Class="Test.SpotlightTour.Pages.ThemesPage"
|
||||
Title="Light & Dark Themes">
|
||||
|
||||
<Grid>
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<Label Text="Theme Support"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center" />
|
||||
|
||||
<Label Text="The tour components support light and dark themes to match your app's appearance."
|
||||
FontSize="14"
|
||||
TextColor="Gray"
|
||||
HorizontalTextAlignment="Center" />
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<!-- Light Theme -->
|
||||
<Frame BackgroundColor="#FAFAFA" BorderColor="#E0E0E0" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<BoxView WidthRequest="24" HeightRequest="24" Color="#FFFFFF" CornerRadius="4" />
|
||||
<Label Text="Light Theme" FontAttributes="Bold" TextColor="#212121" FontSize="16" VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
<Label Text="White backgrounds with dark text. Clean and bright appearance for light mode apps."
|
||||
TextColor="#616161" FontSize="13" />
|
||||
<Button Text="Try Light Theme"
|
||||
BackgroundColor="#1976D2"
|
||||
TextColor="White"
|
||||
Clicked="OnLightThemeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Dark Theme -->
|
||||
<Frame BackgroundColor="#1C1C1E" BorderColor="#3A3A3C" Padding="15" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<BoxView WidthRequest="24" HeightRequest="24" Color="#2C2C2E" CornerRadius="4" />
|
||||
<Label Text="Dark Theme" FontAttributes="Bold" TextColor="White" FontSize="16" VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
<Label Text="Dark backgrounds with light text. Easy on the eyes in low-light environments."
|
||||
TextColor="#ABABAB" FontSize="13" />
|
||||
<Button Text="Try Dark Theme"
|
||||
BackgroundColor="#0A84FF"
|
||||
TextColor="White"
|
||||
Clicked="OnDarkThemeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- System Theme -->
|
||||
<Frame Padding="15" CornerRadius="8">
|
||||
<Frame.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#FAFAFA" Offset="0.0" />
|
||||
<GradientStop Color="#1C1C1E" Offset="1.0" />
|
||||
</LinearGradientBrush>
|
||||
</Frame.Background>
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<Grid WidthRequest="24" HeightRequest="24">
|
||||
<BoxView Color="#FFFFFF" CornerRadius="4" />
|
||||
<BoxView Color="#2C2C2E" CornerRadius="4" Opacity="0.5" />
|
||||
</Grid>
|
||||
<Label Text="System Theme" FontAttributes="Bold" TextColor="#212121" FontSize="16" VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
<Label Text="Automatically follows your device's light/dark mode setting."
|
||||
TextColor="#616161" FontSize="13" />
|
||||
<Button Text="Try System Theme"
|
||||
BackgroundColor="#34C759"
|
||||
TextColor="White"
|
||||
Clicked="OnSystemThemeClicked"
|
||||
CornerRadius="8" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<BoxView HeightRequest="20" Color="Transparent" />
|
||||
|
||||
<!-- Sample elements to highlight -->
|
||||
<Label Text="Sample Elements:"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold" />
|
||||
|
||||
<Frame BackgroundColor="#E8F5E9"
|
||||
Padding="20"
|
||||
CornerRadius="12"
|
||||
spot:Onboarding.StepKey="feature1"
|
||||
spot:Onboarding.Title="Dashboard Overview"
|
||||
spot:Onboarding.Description="Your dashboard shows key metrics and recent activity at a glance. The theme affects all tour UI elements."
|
||||
spot:Onboarding.Order="1">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Dashboard" FontAttributes="Bold" FontSize="16" TextColor="#2E7D32" />
|
||||
<Label Text="View your stats and analytics" TextColor="#4CAF50" FontSize="13" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#E3F2FD"
|
||||
Padding="20"
|
||||
CornerRadius="12"
|
||||
spot:Onboarding.StepKey="feature2"
|
||||
spot:Onboarding.Title="Settings Panel"
|
||||
spot:Onboarding.Description="Customize your experience with various settings. Notice how the callout card matches the selected theme."
|
||||
spot:Onboarding.Order="2">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Settings" FontAttributes="Bold" FontSize="16" TextColor="#1565C0" />
|
||||
<Label Text="Personalize your preferences" TextColor="#2196F3" FontSize="13" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#FFF3E0"
|
||||
Padding="20"
|
||||
CornerRadius="12"
|
||||
spot:Onboarding.StepKey="feature3"
|
||||
spot:Onboarding.Title="Notifications"
|
||||
spot:Onboarding.Description="Stay updated with real-time notifications. The corner navigator also adapts to the theme."
|
||||
spot:Onboarding.Order="3">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Notifications" FontAttributes="Bold" FontSize="16" TextColor="#E65100" />
|
||||
<Label Text="Never miss an update" TextColor="#FF9800" FontSize="13" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<Frame BackgroundColor="#FCE4EC"
|
||||
Padding="20"
|
||||
CornerRadius="12"
|
||||
spot:Onboarding.StepKey="feature4"
|
||||
spot:Onboarding.Title="Profile Section"
|
||||
spot:Onboarding.Description="Manage your profile and account settings here. All UI elements maintain theme consistency."
|
||||
spot:Onboarding.Order="4">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Label Text="Profile" FontAttributes="Bold" FontSize="16" TextColor="#AD1457" />
|
||||
<Label Text="Your account details" TextColor="#E91E63" FontSize="13" />
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<spot:OnboardingHost x:Name="OnboardingHost"
|
||||
ShowCornerNavigator="True"
|
||||
CornerNavigatorPlacement="BottomRight" />
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
32
Test.SpotlightTour/Pages/ThemesPage.xaml.cs
Normal file
32
Test.SpotlightTour/Pages/ThemesPage.xaml.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using MarketAlly.MASpotlightTour;
|
||||
|
||||
namespace Test.SpotlightTour.Pages;
|
||||
|
||||
public partial class ThemesPage : ContentPage
|
||||
{
|
||||
public ThemesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void OnLightThemeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.Theme = TourTheme.Light;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnDarkThemeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.Theme = TourTheme.Dark;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
|
||||
private async void OnSystemThemeClicked(object? sender, EventArgs e)
|
||||
{
|
||||
OnboardingHost.Theme = TourTheme.System;
|
||||
OnboardingHost.DisplayMode = TourDisplayMode.SpotlightWithCallout;
|
||||
await OnboardingHost.StartTourAsync(this.Content);
|
||||
}
|
||||
}
|
||||
6
Test.SpotlightTour/Platforms/Android/AndroidManifest.xml
Normal file
6
Test.SpotlightTour/Platforms/Android/AndroidManifest.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
11
Test.SpotlightTour/Platforms/Android/MainActivity.cs
Normal file
11
Test.SpotlightTour/Platforms/Android/MainActivity.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
}
|
||||
}
|
||||
16
Test.SpotlightTour/Platforms/Android/MainApplication.cs
Normal file
16
Test.SpotlightTour/Platforms/Android/MainApplication.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
[Application]
|
||||
public class MainApplication : MauiApplication
|
||||
{
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
||||
: base(handle, ownership)
|
||||
{
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#512BD4</color>
|
||||
<color name="colorPrimaryDark">#2B0B98</color>
|
||||
<color name="colorAccent">#2B0B98</color>
|
||||
</resources>
|
||||
10
Test.SpotlightTour/Platforms/MacCatalyst/AppDelegate.cs
Normal file
10
Test.SpotlightTour/Platforms/MacCatalyst/AppDelegate.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
14
Test.SpotlightTour/Platforms/MacCatalyst/Entitlements.plist
Normal file
14
Test.SpotlightTour/Platforms/MacCatalyst/Entitlements.plist
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
|
||||
<dict>
|
||||
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
38
Test.SpotlightTour/Platforms/MacCatalyst/Info.plist
Normal file
38
Test.SpotlightTour/Platforms/MacCatalyst/Info.plist
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- The Mac App Store requires you specify if the app uses encryption. -->
|
||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
|
||||
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
|
||||
<!-- Please indicate <true/> or <false/> here. -->
|
||||
|
||||
<!-- Specify the category for your app here. -->
|
||||
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
|
||||
<!-- <key>LSApplicationCategoryType</key> -->
|
||||
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
</dict>
|
||||
</plist>
|
||||
16
Test.SpotlightTour/Platforms/MacCatalyst/Program.cs
Normal file
16
Test.SpotlightTour/Platforms/MacCatalyst/Program.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
// This is the main entry point of the application.
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
||||
// you can specify it here.
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Test.SpotlightTour/Platforms/Tizen/Main.cs
Normal file
17
Test.SpotlightTour/Platforms/Tizen/Main.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Hosting;
|
||||
using System;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
internal class Program : MauiApplication
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var app = new Program();
|
||||
app.Run(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Test.SpotlightTour/Platforms/Tizen/tizen-manifest.xml
Normal file
15
Test.SpotlightTour/Platforms/Tizen/tizen-manifest.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="9" xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common" />
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Test.SpotlightTour.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<label>maui-application-title-placeholder</label>
|
||||
<icon>maui-appicon-placeholder</icon>
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
||||
</ui-application>
|
||||
<shortcut-list />
|
||||
<privileges>
|
||||
<privilege>http://tizen.org/privilege/internet</privilege>
|
||||
</privileges>
|
||||
<dependencies />
|
||||
<provides-appdefined-privileges />
|
||||
</manifest>
|
||||
8
Test.SpotlightTour/Platforms/Windows/App.xaml
Normal file
8
Test.SpotlightTour/Platforms/Windows/App.xaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<maui:MauiWinUIApplication
|
||||
x:Class="Test.SpotlightTour.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maui="using:Microsoft.Maui"
|
||||
xmlns:local="using:Test.SpotlightTour.WinUI">
|
||||
|
||||
</maui:MauiWinUIApplication>
|
||||
25
Test.SpotlightTour/Platforms/Windows/App.xaml.cs
Normal file
25
Test.SpotlightTour/Platforms/Windows/App.xaml.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Test.SpotlightTour.WinUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : MauiWinUIApplication
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
|
||||
}
|
||||
46
Test.SpotlightTour/Platforms/Windows/Package.appxmanifest
Normal file
46
Test.SpotlightTour/Platforms/Windows/Package.appxmanifest
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="C30BB1A4-3F1A-43C0-BA33-BCAC20DB2220" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>$placeholder$</DisplayName>
|
||||
<PublisherDisplayName>User Name</PublisherDisplayName>
|
||||
<Logo>$placeholder$.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate" />
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="$placeholder$"
|
||||
Description="$placeholder$"
|
||||
Square150x150Logo="$placeholder$.png"
|
||||
Square44x44Logo="$placeholder$.png"
|
||||
BackgroundColor="transparent">
|
||||
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
|
||||
<uap:SplashScreen Image="$placeholder$.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
|
||||
</Package>
|
||||
15
Test.SpotlightTour/Platforms/Windows/app.manifest
Normal file
15
Test.SpotlightTour/Platforms/Windows/app.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Test.SpotlightTour.WinUI.app"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
10
Test.SpotlightTour/Platforms/iOS/AppDelegate.cs
Normal file
10
Test.SpotlightTour/Platforms/iOS/AppDelegate.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
}
|
||||
32
Test.SpotlightTour/Platforms/iOS/Info.plist
Normal file
32
Test.SpotlightTour/Platforms/iOS/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/appicon.appiconset</string>
|
||||
</dict>
|
||||
</plist>
|
||||
16
Test.SpotlightTour/Platforms/iOS/Program.cs
Normal file
16
Test.SpotlightTour/Platforms/iOS/Program.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Test.SpotlightTour
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
// This is the main entry point of the application.
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// if you want to use a different Application Delegate class from "AppDelegate"
|
||||
// you can specify it here.
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
|
||||
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
|
||||
|
||||
You are responsible for adding extra entries as needed for your application.
|
||||
|
||||
More information: https://aka.ms/maui-privacy-manifest
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!--
|
||||
The entry below is only needed when you're using the Preferences API in your app.
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict> -->
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
8
Test.SpotlightTour/Properties/launchSettings.json
Normal file
8
Test.SpotlightTour/Properties/launchSettings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false
|
||||
}
|
||||
}
|
||||
}
|
||||
4
Test.SpotlightTour/Resources/AppIcon/appicon.svg
Normal file
4
Test.SpotlightTour/Resources/AppIcon/appicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
8
Test.SpotlightTour/Resources/AppIcon/appiconfg.svg
Normal file
8
Test.SpotlightTour/Resources/AppIcon/appiconfg.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
Test.SpotlightTour/Resources/Fonts/OpenSans-Regular.ttf
Normal file
BIN
Test.SpotlightTour/Resources/Fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
Test.SpotlightTour/Resources/Fonts/OpenSans-Semibold.ttf
Normal file
BIN
Test.SpotlightTour/Resources/Fonts/OpenSans-Semibold.ttf
Normal file
Binary file not shown.
BIN
Test.SpotlightTour/Resources/Images/dotnet_bot.png
Normal file
BIN
Test.SpotlightTour/Resources/Images/dotnet_bot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
15
Test.SpotlightTour/Resources/Raw/AboutAssets.txt
Normal file
15
Test.SpotlightTour/Resources/Raw/AboutAssets.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Any raw assets you want to be deployed with your application can be placed in
|
||||
this directory (and child directories). Deployment of the asset to your application
|
||||
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
|
||||
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
|
||||
These files will be deployed with your package and will be accessible using Essentials:
|
||||
|
||||
async Task LoadMauiAsset()
|
||||
{
|
||||
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var contents = reader.ReadToEnd();
|
||||
}
|
||||
8
Test.SpotlightTour/Resources/Splash/splash.svg
Normal file
8
Test.SpotlightTour/Resources/Splash/splash.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
45
Test.SpotlightTour/Resources/Styles/Colors.xaml
Normal file
45
Test.SpotlightTour/Resources/Styles/Colors.xaml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
|
||||
|
||||
<Color x:Key="Primary">#512BD4</Color>
|
||||
<Color x:Key="PrimaryDark">#ac99ea</Color>
|
||||
<Color x:Key="PrimaryDarkText">#242424</Color>
|
||||
<Color x:Key="Secondary">#DFD8F7</Color>
|
||||
<Color x:Key="SecondaryDarkText">#9880e5</Color>
|
||||
<Color x:Key="Tertiary">#2B0B98</Color>
|
||||
|
||||
<Color x:Key="White">White</Color>
|
||||
<Color x:Key="Black">Black</Color>
|
||||
<Color x:Key="Magenta">#D600AA</Color>
|
||||
<Color x:Key="MidnightBlue">#190649</Color>
|
||||
<Color x:Key="OffBlack">#1f1f1f</Color>
|
||||
|
||||
<Color x:Key="Gray100">#E1E1E1</Color>
|
||||
<Color x:Key="Gray200">#C8C8C8</Color>
|
||||
<Color x:Key="Gray300">#ACACAC</Color>
|
||||
<Color x:Key="Gray400">#919191</Color>
|
||||
<Color x:Key="Gray500">#6E6E6E</Color>
|
||||
<Color x:Key="Gray600">#404040</Color>
|
||||
<Color x:Key="Gray900">#212121</Color>
|
||||
<Color x:Key="Gray950">#141414</Color>
|
||||
|
||||
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
||||
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
||||
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
||||
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
||||
|
||||
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
|
||||
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
|
||||
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
|
||||
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
|
||||
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
|
||||
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
|
||||
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
|
||||
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
|
||||
</ResourceDictionary>
|
||||
440
Test.SpotlightTour/Resources/Styles/Styles.xaml
Normal file
440
Test.SpotlightTour/Resources/Styles/Styles.xaml
Normal file
@@ -0,0 +1,440 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<?xaml-comp compile="true" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||
|
||||
<Style TargetType="ActivityIndicator">
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="IndicatorView">
|
||||
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
|
||||
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="StrokeShape" Value="Rectangle"/>
|
||||
<Setter Property="StrokeThickness" Value="1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="BoxView">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Padding" Value="14,10"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOver" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DatePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Editor">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Entry">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ImageButton">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
<Setter Property="BorderColor" Value="Transparent"/>
|
||||
<Setter Property="BorderWidth" Value="0"/>
|
||||
<Setter Property="CornerRadius" Value="0"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="PointerOver" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="Headline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="32" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Label" x:Key="SubHeadline">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontSize" Value="24" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListView">
|
||||
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Picker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ProgressBar">
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RefreshView">
|
||||
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchBar">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SearchHandler">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Shadow">
|
||||
<Setter Property="Radius" Value="15" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Offset" Value="10,10" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="SwipeItem">
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Switch">
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
<Setter Property="ThumbColor" Value="{StaticResource White}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="On">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Off">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TimePicker">
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
<Style TargetType="TitleBar">
|
||||
<Setter Property="MinimumHeightRequest" Value="32"/>
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="TitleActiveStates">
|
||||
<VisualState x:Name="TitleBarTitleActive">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TitleBarTitleInactive">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
-->
|
||||
|
||||
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
||||
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
||||
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
||||
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
||||
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="NavigationPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TabbedPage">
|
||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
||||
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
||||
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
71
Test.SpotlightTour/Test.SpotlightTour.csproj
Normal file
71
Test.SpotlightTour/Test.SpotlightTour.csproj
Normal file
@@ -0,0 +1,71 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
|
||||
|
||||
<!-- Note for MacCatalyst:
|
||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
||||
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
|
||||
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
|
||||
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
|
||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
||||
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Test.SpotlightTour</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>Test.SpotlightTour</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.companyname.test.spotlighttour</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- App Icon -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||
|
||||
<!-- Splash Screen -->
|
||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
||||
|
||||
<!-- Images -->
|
||||
<MauiImage Include="Resources\Images\*" />
|
||||
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<MauiFont Include="Resources\Fonts\*" />
|
||||
|
||||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MarketAlly.MASpotlightTour\MASpotlightTour.Maui.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user