feat(scenarios): add buy me a coffee maui library
Create a .NET MAUI library for Buy Me a Coffee integration with branded button, QR code, and widget controls. Includes 8 theme presets (yellow, black, white, blue, violet, orange, red, green), customizable styling, and SkiaSharp-based rendering. Supports opening BMC pages in browser and generating QR codes for donations.
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
## Visual Studio
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
## NuGet
|
||||||
|
*.nupkg
|
||||||
|
packages/
|
||||||
|
|
||||||
|
## OS files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
## Rider
|
||||||
|
.idea/
|
||||||
16
BuyMeCofee.Maui/BuyMeACoffeeExtensions.cs
Normal file
16
BuyMeCofee.Maui/BuyMeACoffeeExtensions.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using SkiaSharp.Views.Maui.Controls.Hosting;
|
||||||
|
|
||||||
|
namespace BuyMeCofee.Maui;
|
||||||
|
|
||||||
|
public static class BuyMeACoffeeExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers Buy Me a Coffee controls and their dependencies (SkiaSharp).
|
||||||
|
/// Call this in your MauiProgram.cs CreateMauiApp() builder.
|
||||||
|
/// </summary>
|
||||||
|
public static MauiAppBuilder UseBuyMeACoffee(this MauiAppBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseSkiaSharp();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
BuyMeCofee.Maui/BuyMeCofee.Maui.csproj
Normal file
39
BuyMeCofee.Maui/BuyMeCofee.Maui.csproj
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net10.0-android</TargetFrameworks>
|
||||||
|
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('linux'))">$(TargetFrameworks);net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
|
||||||
|
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
|
||||||
|
|
||||||
|
<UseMaui>true</UseMaui>
|
||||||
|
<SingleProject>true</SingleProject>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<!-- Enable XAML source generation for faster build times and improved performance.
|
||||||
|
This generates C# code from XAML at compile time instead of runtime inflation.
|
||||||
|
To disable, remove this line.
|
||||||
|
For individual files, you can override by setting Inflator metadata:
|
||||||
|
<MauiXaml Update="MyPage.xaml" Inflator="Default" /> (reverts to defaults: Runtime for Debug, XamlC for Release)
|
||||||
|
<MauiXaml Update="MyPage.xaml" Inflator="Runtime" /> (force runtime inflation) -->
|
||||||
|
<MauiXamlInflator>SourceGen</MauiXamlInflator>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
||||||
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
|
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="3.116.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\Images\bmc_logo.png"
|
||||||
|
LogicalName="BuyMeCofee.Maui.Resources.Images.bmc_logo.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
21
BuyMeCofee.Maui/Constants/BmcColors.cs
Normal file
21
BuyMeCofee.Maui/Constants/BmcColors.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Constants;
|
||||||
|
|
||||||
|
public static class BmcColors
|
||||||
|
{
|
||||||
|
public const string CupYellow = "#FFDD00";
|
||||||
|
public const string BrandDark = "#0D0C22";
|
||||||
|
|
||||||
|
// Button theme backgrounds
|
||||||
|
public const string YellowBg = "#FFDD00";
|
||||||
|
public const string BlackBg = "#0D0C22";
|
||||||
|
public const string WhiteBg = "#FFFFFF";
|
||||||
|
public const string BlueBg = "#5F7FFF";
|
||||||
|
public const string VioletBg = "#BD5CFF";
|
||||||
|
public const string OrangeBg = "#FF813F";
|
||||||
|
public const string RedBg = "#FF6073";
|
||||||
|
public const string GreenBg = "#78DEC7";
|
||||||
|
|
||||||
|
// Widget
|
||||||
|
public const string WidgetPurple = "#6C5CE7";
|
||||||
|
public const string WhiteStroke = "#E0E0E0";
|
||||||
|
}
|
||||||
11
BuyMeCofee.Maui/Constants/BmcConstants.cs
Normal file
11
BuyMeCofee.Maui/Constants/BmcConstants.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Constants;
|
||||||
|
|
||||||
|
public static class BmcConstants
|
||||||
|
{
|
||||||
|
public const string BaseUrl = "https://buymeacoffee.com/";
|
||||||
|
public const string DefaultButtonText = "Buy me a coffee";
|
||||||
|
public const string DefaultSupportButtonText = "Support";
|
||||||
|
public const double ButtonCornerRadius = 8.0;
|
||||||
|
public const double WidgetCornerRadius = 16.0;
|
||||||
|
public static readonly int[] DefaultSuggestedAmounts = [25, 50, 100];
|
||||||
|
}
|
||||||
233
BuyMeCofee.Maui/Controls/BuyMeACoffeeButton.cs
Normal file
233
BuyMeCofee.Maui/Controls/BuyMeACoffeeButton.cs
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
using BuyMeCofee.Maui.Constants;
|
||||||
|
using BuyMeCofee.Maui.Enums;
|
||||||
|
using BuyMeCofee.Maui.Helpers;
|
||||||
|
using Microsoft.Maui.Controls.Shapes;
|
||||||
|
|
||||||
|
namespace BuyMeCofee.Maui.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A branded Buy Me a Coffee button with the official cup logo and 8 color theme presets.
|
||||||
|
/// Opens the user's BMC page in the default browser when tapped.
|
||||||
|
/// </summary>
|
||||||
|
public class BuyMeACoffeeButton : ContentView
|
||||||
|
{
|
||||||
|
#region Bindable Properties
|
||||||
|
|
||||||
|
public static readonly BindableProperty UsernameProperty =
|
||||||
|
BindableProperty.Create(nameof(Username), typeof(string), typeof(BuyMeACoffeeButton),
|
||||||
|
string.Empty);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ButtonTextProperty =
|
||||||
|
BindableProperty.Create(nameof(ButtonText), typeof(string), typeof(BuyMeACoffeeButton),
|
||||||
|
BmcConstants.DefaultButtonText, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ThemeProperty =
|
||||||
|
BindableProperty.Create(nameof(Theme), typeof(BmcButtonTheme), typeof(BuyMeACoffeeButton),
|
||||||
|
BmcButtonTheme.Yellow, propertyChanged: OnThemePropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CustomBackgroundColorProperty =
|
||||||
|
BindableProperty.Create(nameof(CustomBackgroundColor), typeof(Color), typeof(BuyMeACoffeeButton),
|
||||||
|
null, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CustomTextColorProperty =
|
||||||
|
BindableProperty.Create(nameof(CustomTextColor), typeof(Color), typeof(BuyMeACoffeeButton),
|
||||||
|
null, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CornerRadiusProperty =
|
||||||
|
BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(BuyMeACoffeeButton),
|
||||||
|
BmcConstants.ButtonCornerRadius, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty FontSizeProperty =
|
||||||
|
BindableProperty.Create(nameof(FontSize), typeof(double), typeof(BuyMeACoffeeButton),
|
||||||
|
16.0, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty CupSizeProperty =
|
||||||
|
BindableProperty.Create(nameof(CupSize), typeof(double), typeof(BuyMeACoffeeButton),
|
||||||
|
28.0, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>Your Buy Me a Coffee username/slug.</summary>
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => (string)GetValue(UsernameProperty);
|
||||||
|
set => SetValue(UsernameProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Button label text. Default: "Buy me a coffee"</summary>
|
||||||
|
public string ButtonText
|
||||||
|
{
|
||||||
|
get => (string)GetValue(ButtonTextProperty);
|
||||||
|
set => SetValue(ButtonTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Color theme preset. Default: Yellow (official BMC brand color).</summary>
|
||||||
|
public BmcButtonTheme Theme
|
||||||
|
{
|
||||||
|
get => (BmcButtonTheme)GetValue(ThemeProperty);
|
||||||
|
set => SetValue(ThemeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Custom background color. Only used when Theme is set to Custom.</summary>
|
||||||
|
public Color? CustomBackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color?)GetValue(CustomBackgroundColorProperty);
|
||||||
|
set => SetValue(CustomBackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Custom text color. Only used when Theme is set to Custom.</summary>
|
||||||
|
public Color? CustomTextColor
|
||||||
|
{
|
||||||
|
get => (Color?)GetValue(CustomTextColorProperty);
|
||||||
|
set => SetValue(CustomTextColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Border corner radius. Default: 8</summary>
|
||||||
|
public double CornerRadius
|
||||||
|
{
|
||||||
|
get => (double)GetValue(CornerRadiusProperty);
|
||||||
|
set => SetValue(CornerRadiusProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Font size for the button text. Default: 16</summary>
|
||||||
|
public double FontSize
|
||||||
|
{
|
||||||
|
get => (double)GetValue(FontSizeProperty);
|
||||||
|
set => SetValue(FontSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cup logo image height. Default: 28</summary>
|
||||||
|
public double CupSize
|
||||||
|
{
|
||||||
|
get => (double)GetValue(CupSizeProperty);
|
||||||
|
set => SetValue(CupSizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private Border _border = null!;
|
||||||
|
private Image _cupImage = null!;
|
||||||
|
private Label _textLabel = null!;
|
||||||
|
|
||||||
|
public BuyMeACoffeeButton()
|
||||||
|
{
|
||||||
|
BuildLayout();
|
||||||
|
ApplyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayout()
|
||||||
|
{
|
||||||
|
_cupImage = new Image
|
||||||
|
{
|
||||||
|
Source = BmcBrandAssets.GetCupLogo(),
|
||||||
|
HeightRequest = CupSize,
|
||||||
|
Aspect = Aspect.AspectFit,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
_textLabel = new Label
|
||||||
|
{
|
||||||
|
Text = ButtonText,
|
||||||
|
FontSize = FontSize,
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
var stack = new HorizontalStackLayout
|
||||||
|
{
|
||||||
|
Spacing = 8,
|
||||||
|
HorizontalOptions = LayoutOptions.Center,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
Children = { _cupImage, _textLabel }
|
||||||
|
};
|
||||||
|
|
||||||
|
_border = new Border
|
||||||
|
{
|
||||||
|
StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(CornerRadius) },
|
||||||
|
Stroke = Colors.Transparent,
|
||||||
|
Padding = new Thickness(16, 10),
|
||||||
|
Content = stack,
|
||||||
|
Shadow = new Shadow
|
||||||
|
{
|
||||||
|
Brush = new SolidColorBrush(Colors.Black),
|
||||||
|
Offset = new Point(0, 2),
|
||||||
|
Radius = 4,
|
||||||
|
Opacity = 0.25f,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var tap = new TapGestureRecognizer();
|
||||||
|
tap.Tapped += OnTapped;
|
||||||
|
_border.GestureRecognizers.Add(tap);
|
||||||
|
|
||||||
|
var hover = new PointerGestureRecognizer();
|
||||||
|
hover.PointerEntered += (_, _) => _border.Opacity = 0.85;
|
||||||
|
hover.PointerExited += (_, _) => _border.Opacity = 1.0;
|
||||||
|
_border.GestureRecognizers.Add(hover);
|
||||||
|
|
||||||
|
Content = _border;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyTheme()
|
||||||
|
{
|
||||||
|
if (_border is null) return;
|
||||||
|
|
||||||
|
Color bg, text;
|
||||||
|
Color stroke = Colors.Transparent;
|
||||||
|
|
||||||
|
if (Theme == BmcButtonTheme.Custom)
|
||||||
|
{
|
||||||
|
bg = CustomBackgroundColor ?? Color.FromArgb(BmcColors.YellowBg);
|
||||||
|
text = CustomTextColor ?? Color.FromArgb(BmcColors.BrandDark);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var info = BmcThemeResolver.Resolve(Theme);
|
||||||
|
bg = info.Background;
|
||||||
|
text = info.TextColor;
|
||||||
|
stroke = info.StrokeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
_border.BackgroundColor = bg;
|
||||||
|
_border.Stroke = new SolidColorBrush(stroke);
|
||||||
|
_textLabel.TextColor = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisuals()
|
||||||
|
{
|
||||||
|
if (_border is null) return;
|
||||||
|
|
||||||
|
_textLabel.Text = ButtonText;
|
||||||
|
_textLabel.FontSize = FontSize;
|
||||||
|
_cupImage.HeightRequest = CupSize;
|
||||||
|
|
||||||
|
if (_border.StrokeShape is RoundRectangle rr)
|
||||||
|
rr.CornerRadius = new Microsoft.Maui.CornerRadius(CornerRadius);
|
||||||
|
|
||||||
|
ApplyTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Username)) return;
|
||||||
|
|
||||||
|
await _border.ScaleToAsync(0.95, 80, Easing.CubicIn);
|
||||||
|
await _border.ScaleToAsync(1.0, 80, Easing.CubicOut);
|
||||||
|
|
||||||
|
await Launcher.Default.OpenAsync(new Uri($"{BmcConstants.BaseUrl}{Username}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnVisualPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeButton button)
|
||||||
|
button.UpdateVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnThemePropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeButton button)
|
||||||
|
button.ApplyTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
219
BuyMeCofee.Maui/Controls/BuyMeACoffeeQrCode.cs
Normal file
219
BuyMeCofee.Maui/Controls/BuyMeACoffeeQrCode.cs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
using BuyMeCofee.Maui.Constants;
|
||||||
|
using BuyMeCofee.Maui.Helpers;
|
||||||
|
using QRCoder;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.Views.Maui;
|
||||||
|
using SkiaSharp.Views.Maui.Controls;
|
||||||
|
|
||||||
|
namespace BuyMeCofee.Maui.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates and displays a QR code linking to a Buy Me a Coffee profile,
|
||||||
|
/// with the BMC coffee cup logo overlaid in the center.
|
||||||
|
/// </summary>
|
||||||
|
public class BuyMeACoffeeQrCode : ContentView
|
||||||
|
{
|
||||||
|
#region Bindable Properties
|
||||||
|
|
||||||
|
public static readonly BindableProperty UsernameProperty =
|
||||||
|
BindableProperty.Create(nameof(Username), typeof(string), typeof(BuyMeACoffeeQrCode),
|
||||||
|
string.Empty, propertyChanged: OnQrPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty SizeProperty =
|
||||||
|
BindableProperty.Create(nameof(Size), typeof(double), typeof(BuyMeACoffeeQrCode),
|
||||||
|
200.0, propertyChanged: OnSizeChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ForegroundColorProperty =
|
||||||
|
BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(BuyMeACoffeeQrCode),
|
||||||
|
Colors.Black, propertyChanged: OnQrPropertyChanged);
|
||||||
|
|
||||||
|
public static new readonly BindableProperty BackgroundColorProperty =
|
||||||
|
BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(BuyMeACoffeeQrCode),
|
||||||
|
Colors.White, propertyChanged: OnQrPropertyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty LogoSizeFractionProperty =
|
||||||
|
BindableProperty.Create(nameof(LogoSizeFraction), typeof(double), typeof(BuyMeACoffeeQrCode),
|
||||||
|
0.25, propertyChanged: OnVisualPropertyChanged);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>Your Buy Me a Coffee username/slug.</summary>
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => (string)GetValue(UsernameProperty);
|
||||||
|
set => SetValue(UsernameProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Width and height of the QR code control. Default: 200</summary>
|
||||||
|
public double Size
|
||||||
|
{
|
||||||
|
get => (double)GetValue(SizeProperty);
|
||||||
|
set => SetValue(SizeProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>QR code foreground (module) color. Default: Black</summary>
|
||||||
|
public Color ForegroundColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(ForegroundColorProperty);
|
||||||
|
set => SetValue(ForegroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>QR code background color. Default: White</summary>
|
||||||
|
public new Color BackgroundColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(BackgroundColorProperty);
|
||||||
|
set => SetValue(BackgroundColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Logo size as fraction of QR code size (0.0 - 0.35). Default: 0.25</summary>
|
||||||
|
public double LogoSizeFraction
|
||||||
|
{
|
||||||
|
get => (double)GetValue(LogoSizeFractionProperty);
|
||||||
|
set => SetValue(LogoSizeFractionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private readonly SKCanvasView _canvas;
|
||||||
|
private SKBitmap? _qrBitmap;
|
||||||
|
private SKBitmap? _logoBitmap;
|
||||||
|
|
||||||
|
public BuyMeACoffeeQrCode()
|
||||||
|
{
|
||||||
|
_canvas = new SKCanvasView
|
||||||
|
{
|
||||||
|
WidthRequest = Size,
|
||||||
|
HeightRequest = Size,
|
||||||
|
};
|
||||||
|
_canvas.PaintSurface += OnPaintSurface;
|
||||||
|
|
||||||
|
Content = _canvas;
|
||||||
|
LoadLogo();
|
||||||
|
RegenerateQr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadLogo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = BmcBrandAssets.GetCupLogoStream();
|
||||||
|
_logoBitmap = SKBitmap.Decode(stream);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_logoBitmap = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegenerateQr()
|
||||||
|
{
|
||||||
|
_qrBitmap?.Dispose();
|
||||||
|
_qrBitmap = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Username))
|
||||||
|
{
|
||||||
|
_canvas.InvalidateSurface();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = $"{BmcConstants.BaseUrl}{Username}";
|
||||||
|
|
||||||
|
using var generator = new QRCodeGenerator();
|
||||||
|
var data = generator.CreateQrCode(url, QRCodeGenerator.ECCLevel.H);
|
||||||
|
var renderer = new PngByteQRCode(data);
|
||||||
|
|
||||||
|
var darkRgba = ColorToRgba(ForegroundColor);
|
||||||
|
var lightRgba = ColorToRgba(BackgroundColor);
|
||||||
|
var pngBytes = renderer.GetGraphic(20, darkRgba, lightRgba);
|
||||||
|
|
||||||
|
using var stream = new MemoryStream(pngBytes);
|
||||||
|
_qrBitmap = SKBitmap.Decode(stream);
|
||||||
|
|
||||||
|
_canvas.InvalidateSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
||||||
|
{
|
||||||
|
var canvas = e.Surface.Canvas;
|
||||||
|
canvas.Clear(SKColors.Transparent);
|
||||||
|
|
||||||
|
if (_qrBitmap is null) return;
|
||||||
|
|
||||||
|
var info = e.Info;
|
||||||
|
var size = Math.Min(info.Width, info.Height);
|
||||||
|
|
||||||
|
// Draw QR code scaled to fill
|
||||||
|
var qrRect = new SKRect(0, 0, size, size);
|
||||||
|
using var qrPaint = new SKPaint { IsAntialias = false };
|
||||||
|
canvas.DrawBitmap(_qrBitmap, qrRect, qrPaint);
|
||||||
|
|
||||||
|
if (_logoBitmap is null) return;
|
||||||
|
|
||||||
|
// Draw white quiet zone behind logo
|
||||||
|
var logoSize = (float)(size * Math.Clamp(LogoSizeFraction, 0.0, 0.35));
|
||||||
|
var logoPadding = logoSize * 0.15f;
|
||||||
|
var cx = size / 2f;
|
||||||
|
var cy = size / 2f;
|
||||||
|
|
||||||
|
var quietRect = new SKRect(
|
||||||
|
cx - logoSize / 2 - logoPadding,
|
||||||
|
cy - logoSize / 2 - logoPadding,
|
||||||
|
cx + logoSize / 2 + logoPadding,
|
||||||
|
cy + logoSize / 2 + logoPadding);
|
||||||
|
|
||||||
|
using var quietPaint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = SKColors.White,
|
||||||
|
IsAntialias = true,
|
||||||
|
Style = SKPaintStyle.Fill,
|
||||||
|
};
|
||||||
|
canvas.DrawRoundRect(quietRect, 8, 8, quietPaint);
|
||||||
|
|
||||||
|
// Draw logo centered
|
||||||
|
var logoRect = new SKRect(
|
||||||
|
cx - logoSize / 2, cy - logoSize / 2,
|
||||||
|
cx + logoSize / 2, cy + logoSize / 2);
|
||||||
|
using var logoPaint = new SKPaint { IsAntialias = true };
|
||||||
|
canvas.DrawBitmap(_logoBitmap, logoRect, logoPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ColorToRgba(Color color)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
(byte)(color.Red * 255),
|
||||||
|
(byte)(color.Green * 255),
|
||||||
|
(byte)(color.Blue * 255),
|
||||||
|
(byte)(color.Alpha * 255),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Property Changed Handlers
|
||||||
|
|
||||||
|
private static void OnQrPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeQrCode qr)
|
||||||
|
qr.RegenerateQr();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnVisualPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeQrCode qr)
|
||||||
|
qr._canvas.InvalidateSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSizeChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeQrCode qr)
|
||||||
|
{
|
||||||
|
var size = (double)newValue;
|
||||||
|
qr._canvas.WidthRequest = size;
|
||||||
|
qr._canvas.HeightRequest = size;
|
||||||
|
qr._canvas.InvalidateSurface();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
442
BuyMeCofee.Maui/Controls/BuyMeACoffeeWidget.cs
Normal file
442
BuyMeCofee.Maui/Controls/BuyMeACoffeeWidget.cs
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
using BuyMeCofee.Maui.Constants;
|
||||||
|
using BuyMeCofee.Maui.Helpers;
|
||||||
|
using BuyMeCofee.Maui.Models;
|
||||||
|
using Microsoft.Maui.Controls.Shapes;
|
||||||
|
|
||||||
|
namespace BuyMeCofee.Maui.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A branded Buy Me a Coffee support widget with amount entry, preset chips,
|
||||||
|
/// name/message fields, and a monthly toggle. Opens the BMC page on support tap.
|
||||||
|
/// </summary>
|
||||||
|
public class BuyMeACoffeeWidget : ContentView
|
||||||
|
{
|
||||||
|
#region Bindable Properties
|
||||||
|
|
||||||
|
public static readonly BindableProperty UsernameProperty =
|
||||||
|
BindableProperty.Create(nameof(Username), typeof(string), typeof(BuyMeACoffeeWidget),
|
||||||
|
string.Empty, propertyChanged: OnUsernameChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty DisplayNameProperty =
|
||||||
|
BindableProperty.Create(nameof(DisplayName), typeof(string), typeof(BuyMeACoffeeWidget),
|
||||||
|
string.Empty, propertyChanged: OnDisplayNameChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty SuggestedAmountsProperty =
|
||||||
|
BindableProperty.Create(nameof(SuggestedAmounts), typeof(int[]), typeof(BuyMeACoffeeWidget),
|
||||||
|
BmcConstants.DefaultSuggestedAmounts, propertyChanged: OnChipsChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty DefaultAmountProperty =
|
||||||
|
BindableProperty.Create(nameof(DefaultAmount), typeof(int), typeof(BuyMeACoffeeWidget),
|
||||||
|
5, propertyChanged: OnDefaultAmountChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty AccentColorProperty =
|
||||||
|
BindableProperty.Create(nameof(AccentColor), typeof(Color), typeof(BuyMeACoffeeWidget),
|
||||||
|
Color.FromArgb(BmcColors.WidgetPurple), propertyChanged: OnAccentChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty ShowMonthlyOptionProperty =
|
||||||
|
BindableProperty.Create(nameof(ShowMonthlyOption), typeof(bool), typeof(BuyMeACoffeeWidget),
|
||||||
|
true, propertyChanged: OnShowMonthlyChanged);
|
||||||
|
|
||||||
|
public static readonly BindableProperty SupportButtonTextProperty =
|
||||||
|
BindableProperty.Create(nameof(SupportButtonText), typeof(string), typeof(BuyMeACoffeeWidget),
|
||||||
|
BmcConstants.DefaultSupportButtonText);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
/// <summary>Your Buy Me a Coffee username/slug.</summary>
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => (string)GetValue(UsernameProperty);
|
||||||
|
set => SetValue(UsernameProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Name displayed in "Support {DisplayName}" header.</summary>
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get => (string)GetValue(DisplayNameProperty);
|
||||||
|
set => SetValue(DisplayNameProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Preset amount chips. Default: [25, 50, 100]</summary>
|
||||||
|
public int[] SuggestedAmounts
|
||||||
|
{
|
||||||
|
get => (int[])GetValue(SuggestedAmountsProperty);
|
||||||
|
set => SetValue(SuggestedAmountsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initial amount in the entry field. Default: 5</summary>
|
||||||
|
public int DefaultAmount
|
||||||
|
{
|
||||||
|
get => (int)GetValue(DefaultAmountProperty);
|
||||||
|
set => SetValue(DefaultAmountProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Accent color for the Support button. Default: BMC purple (#6C5CE7)</summary>
|
||||||
|
public Color AccentColor
|
||||||
|
{
|
||||||
|
get => (Color)GetValue(AccentColorProperty);
|
||||||
|
set => SetValue(AccentColorProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Whether to show the "Make this monthly" checkbox. Default: true</summary>
|
||||||
|
public bool ShowMonthlyOption
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(ShowMonthlyOptionProperty);
|
||||||
|
set => SetValue(ShowMonthlyOptionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Text on the support button. Default: "Support"</summary>
|
||||||
|
public string SupportButtonText
|
||||||
|
{
|
||||||
|
get => (string)GetValue(SupportButtonTextProperty);
|
||||||
|
set => SetValue(SupportButtonTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>Raised when the user taps the Support button, before opening the browser.</summary>
|
||||||
|
public event EventHandler<SupportRequestedEventArgs>? SupportRequested;
|
||||||
|
|
||||||
|
// Internal UI elements
|
||||||
|
private Label _headerLabel = null!;
|
||||||
|
private Entry _amountEntry = null!;
|
||||||
|
private HorizontalStackLayout _chipsRow = null!;
|
||||||
|
private Entry _nameEntry = null!;
|
||||||
|
private Editor _messageEditor = null!;
|
||||||
|
private CheckBox _monthlyCheckBox = null!;
|
||||||
|
private HorizontalStackLayout _monthlyRow = null!;
|
||||||
|
private Border _supportButton = null!;
|
||||||
|
private Label _supportButtonLabel = null!;
|
||||||
|
private Label _footerLabel = null!;
|
||||||
|
|
||||||
|
private int _currentAmount;
|
||||||
|
|
||||||
|
public BuyMeACoffeeWidget()
|
||||||
|
{
|
||||||
|
_currentAmount = DefaultAmount;
|
||||||
|
BuildLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayout()
|
||||||
|
{
|
||||||
|
// Header
|
||||||
|
_headerLabel = new Label
|
||||||
|
{
|
||||||
|
Text = string.IsNullOrWhiteSpace(DisplayName) ? "Support" : $"Support {DisplayName}",
|
||||||
|
FontSize = 20,
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
Margin = new Thickness(0, 0, 0, 8),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Amount row: $ + Entry + chips
|
||||||
|
var dollarLabel = new Label
|
||||||
|
{
|
||||||
|
Text = "$",
|
||||||
|
FontSize = 18,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
TextColor = Colors.Gray,
|
||||||
|
};
|
||||||
|
|
||||||
|
_amountEntry = new Entry
|
||||||
|
{
|
||||||
|
Text = _currentAmount.ToString(),
|
||||||
|
Keyboard = Keyboard.Numeric,
|
||||||
|
FontSize = 16,
|
||||||
|
Placeholder = "Enter amount",
|
||||||
|
HorizontalOptions = LayoutOptions.Fill,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
_amountEntry.TextChanged += OnAmountTextChanged;
|
||||||
|
|
||||||
|
_chipsRow = new HorizontalStackLayout { Spacing = 6 };
|
||||||
|
BuildChips();
|
||||||
|
|
||||||
|
var amountRow = new Grid
|
||||||
|
{
|
||||||
|
ColumnDefinitions =
|
||||||
|
{
|
||||||
|
new ColumnDefinition(GridLength.Auto),
|
||||||
|
new ColumnDefinition(GridLength.Star),
|
||||||
|
new ColumnDefinition(GridLength.Auto),
|
||||||
|
},
|
||||||
|
ColumnSpacing = 8,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
amountRow.Add(dollarLabel, 0);
|
||||||
|
amountRow.Add(_amountEntry, 1);
|
||||||
|
amountRow.Add(_chipsRow, 2);
|
||||||
|
|
||||||
|
var amountBorder = CreateFieldBorder(amountRow);
|
||||||
|
|
||||||
|
// Name field
|
||||||
|
_nameEntry = new Entry
|
||||||
|
{
|
||||||
|
Placeholder = "Name or @yoursocial",
|
||||||
|
FontSize = 14,
|
||||||
|
};
|
||||||
|
var nameBorder = CreateFieldBorder(_nameEntry);
|
||||||
|
|
||||||
|
// Message field
|
||||||
|
_messageEditor = new Editor
|
||||||
|
{
|
||||||
|
Placeholder = "Say something nice...",
|
||||||
|
FontSize = 14,
|
||||||
|
HeightRequest = 80,
|
||||||
|
AutoSize = EditorAutoSizeOption.TextChanges,
|
||||||
|
};
|
||||||
|
var messageBorder = CreateFieldBorder(_messageEditor);
|
||||||
|
|
||||||
|
// Monthly toggle
|
||||||
|
_monthlyCheckBox = new CheckBox
|
||||||
|
{
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
var monthlyLabel = new Label
|
||||||
|
{
|
||||||
|
Text = "Make this monthly",
|
||||||
|
FontSize = 14,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
_monthlyRow = new HorizontalStackLayout
|
||||||
|
{
|
||||||
|
Spacing = 4,
|
||||||
|
IsVisible = ShowMonthlyOption,
|
||||||
|
Children = { _monthlyCheckBox, monthlyLabel }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Support button
|
||||||
|
_supportButtonLabel = new Label
|
||||||
|
{
|
||||||
|
Text = SupportButtonText,
|
||||||
|
FontSize = 16,
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
TextColor = Colors.White,
|
||||||
|
HorizontalOptions = LayoutOptions.Center,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
_supportButton = new Border
|
||||||
|
{
|
||||||
|
BackgroundColor = AccentColor,
|
||||||
|
StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(24) },
|
||||||
|
Stroke = Colors.Transparent,
|
||||||
|
Padding = new Thickness(0, 14),
|
||||||
|
HorizontalOptions = LayoutOptions.Fill,
|
||||||
|
Content = _supportButtonLabel,
|
||||||
|
};
|
||||||
|
|
||||||
|
var supportTap = new TapGestureRecognizer();
|
||||||
|
supportTap.Tapped += OnSupportTapped;
|
||||||
|
_supportButton.GestureRecognizers.Add(supportTap);
|
||||||
|
|
||||||
|
var supportHover = new PointerGestureRecognizer();
|
||||||
|
supportHover.PointerEntered += (_, _) => _supportButton.Opacity = 0.85;
|
||||||
|
supportHover.PointerExited += (_, _) => _supportButton.Opacity = 1.0;
|
||||||
|
_supportButton.GestureRecognizers.Add(supportHover);
|
||||||
|
|
||||||
|
// Redirect note
|
||||||
|
var redirectNote = new Label
|
||||||
|
{
|
||||||
|
Text = "You'll complete your support on buymeacoffee.com",
|
||||||
|
FontSize = 11,
|
||||||
|
TextColor = Colors.Gray,
|
||||||
|
HorizontalOptions = LayoutOptions.Center,
|
||||||
|
Margin = new Thickness(0, -4, 0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
var footerCup = new Image
|
||||||
|
{
|
||||||
|
Source = BmcBrandAssets.GetCupLogo(),
|
||||||
|
HeightRequest = 16,
|
||||||
|
Aspect = Aspect.AspectFit,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
_footerLabel = new Label
|
||||||
|
{
|
||||||
|
Text = $"buymeacoffee.com/{Username}",
|
||||||
|
FontSize = 12,
|
||||||
|
TextColor = Colors.Gray,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
TextDecorations = TextDecorations.Underline,
|
||||||
|
};
|
||||||
|
var footerStack = new HorizontalStackLayout
|
||||||
|
{
|
||||||
|
Spacing = 4,
|
||||||
|
HorizontalOptions = LayoutOptions.Center,
|
||||||
|
Children = { footerCup, _footerLabel }
|
||||||
|
};
|
||||||
|
var footerTap = new TapGestureRecognizer();
|
||||||
|
footerTap.Tapped += OnFooterTapped;
|
||||||
|
footerStack.GestureRecognizers.Add(footerTap);
|
||||||
|
|
||||||
|
// Card layout
|
||||||
|
var cardContent = new VerticalStackLayout
|
||||||
|
{
|
||||||
|
Spacing = 12,
|
||||||
|
Padding = new Thickness(20),
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
_headerLabel,
|
||||||
|
amountBorder,
|
||||||
|
nameBorder,
|
||||||
|
messageBorder,
|
||||||
|
_monthlyRow,
|
||||||
|
_supportButton,
|
||||||
|
redirectNote,
|
||||||
|
footerStack,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var card = new Border
|
||||||
|
{
|
||||||
|
StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(BmcConstants.WidgetCornerRadius) },
|
||||||
|
Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")),
|
||||||
|
StrokeThickness = 1,
|
||||||
|
BackgroundColor = Colors.White,
|
||||||
|
Shadow = new Shadow
|
||||||
|
{
|
||||||
|
Brush = new SolidColorBrush(Colors.Black),
|
||||||
|
Offset = new Point(0, 4),
|
||||||
|
Radius = 12,
|
||||||
|
Opacity = 0.10f,
|
||||||
|
},
|
||||||
|
Content = cardContent,
|
||||||
|
MaximumWidthRequest = 380,
|
||||||
|
};
|
||||||
|
|
||||||
|
Content = card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border CreateFieldBorder(View content)
|
||||||
|
{
|
||||||
|
return new Border
|
||||||
|
{
|
||||||
|
StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(10) },
|
||||||
|
Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")),
|
||||||
|
StrokeThickness = 1,
|
||||||
|
Padding = new Thickness(12, 6),
|
||||||
|
Content = content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildChips()
|
||||||
|
{
|
||||||
|
_chipsRow.Children.Clear();
|
||||||
|
foreach (var amount in SuggestedAmounts)
|
||||||
|
{
|
||||||
|
var chipLabel = new Label
|
||||||
|
{
|
||||||
|
Text = $"+{amount}",
|
||||||
|
FontSize = 13,
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
TextColor = Color.FromArgb(BmcColors.BrandDark),
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
Padding = new Thickness(10, 4),
|
||||||
|
};
|
||||||
|
|
||||||
|
var chip = new Border
|
||||||
|
{
|
||||||
|
StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(6) },
|
||||||
|
Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")),
|
||||||
|
StrokeThickness = 1,
|
||||||
|
BackgroundColor = Color.FromArgb("#F5F5F5"),
|
||||||
|
Content = chipLabel,
|
||||||
|
VerticalOptions = LayoutOptions.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
var chipTap = new TapGestureRecognizer();
|
||||||
|
var capturedAmount = amount;
|
||||||
|
chipTap.Tapped += (_, _) =>
|
||||||
|
{
|
||||||
|
_currentAmount = capturedAmount;
|
||||||
|
_amountEntry.Text = _currentAmount.ToString();
|
||||||
|
};
|
||||||
|
chip.GestureRecognizers.Add(chipTap);
|
||||||
|
|
||||||
|
_chipsRow.Children.Add(chip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAmountTextChanged(object? sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (int.TryParse(e.NewTextValue, out var amount) && amount >= 0)
|
||||||
|
_currentAmount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSupportTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Username)) return;
|
||||||
|
|
||||||
|
// Fire event with collected data
|
||||||
|
SupportRequested?.Invoke(this, new SupportRequestedEventArgs
|
||||||
|
{
|
||||||
|
Username = Username,
|
||||||
|
Amount = _currentAmount,
|
||||||
|
Name = string.IsNullOrWhiteSpace(_nameEntry.Text) ? null : _nameEntry.Text,
|
||||||
|
Message = string.IsNullOrWhiteSpace(_messageEditor.Text) ? null : _messageEditor.Text,
|
||||||
|
IsMonthly = _monthlyCheckBox.IsChecked,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate
|
||||||
|
await _supportButton.ScaleToAsync(0.95, 80, Easing.CubicIn);
|
||||||
|
await _supportButton.ScaleToAsync(1.0, 80, Easing.CubicOut);
|
||||||
|
|
||||||
|
// Open browser
|
||||||
|
await Launcher.Default.OpenAsync(new Uri($"{BmcConstants.BaseUrl}{Username}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnFooterTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Username)) return;
|
||||||
|
await Launcher.Default.OpenAsync(new Uri($"{BmcConstants.BaseUrl}{Username}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Property Changed Handlers
|
||||||
|
|
||||||
|
private static void OnUsernameChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
widget._footerLabel.Text = $"buymeacoffee.com/{newValue}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnDisplayNameChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
{
|
||||||
|
var name = (string)newValue;
|
||||||
|
widget._headerLabel.Text = string.IsNullOrWhiteSpace(name) ? "Support" : $"Support {name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnChipsChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
widget.BuildChips();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnDefaultAmountChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
{
|
||||||
|
widget._currentAmount = (int)newValue;
|
||||||
|
widget._amountEntry.Text = widget._currentAmount.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnAccentChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
widget._supportButton.BackgroundColor = (Color)newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnShowMonthlyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||||
|
{
|
||||||
|
if (bindable is BuyMeACoffeeWidget widget)
|
||||||
|
widget._monthlyRow.IsVisible = (bool)newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
14
BuyMeCofee.Maui/Enums/BmcButtonTheme.cs
Normal file
14
BuyMeCofee.Maui/Enums/BmcButtonTheme.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Enums;
|
||||||
|
|
||||||
|
public enum BmcButtonTheme
|
||||||
|
{
|
||||||
|
Yellow,
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
Blue,
|
||||||
|
Violet,
|
||||||
|
Orange,
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
18
BuyMeCofee.Maui/Helpers/BmcBrandAssets.cs
Normal file
18
BuyMeCofee.Maui/Helpers/BmcBrandAssets.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Helpers;
|
||||||
|
|
||||||
|
internal static class BmcBrandAssets
|
||||||
|
{
|
||||||
|
private const string LogoResourceName = "BuyMeCofee.Maui.Resources.Images.bmc_logo.png";
|
||||||
|
|
||||||
|
internal static ImageSource GetCupLogo()
|
||||||
|
{
|
||||||
|
var assembly = typeof(BmcBrandAssets).Assembly;
|
||||||
|
return ImageSource.FromStream(() => assembly.GetManifestResourceStream(LogoResourceName)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Stream GetCupLogoStream()
|
||||||
|
{
|
||||||
|
var assembly = typeof(BmcBrandAssets).Assembly;
|
||||||
|
return assembly.GetManifestResourceStream(LogoResourceName)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
BuyMeCofee.Maui/Helpers/BmcThemeResolver.cs
Normal file
23
BuyMeCofee.Maui/Helpers/BmcThemeResolver.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using BuyMeCofee.Maui.Constants;
|
||||||
|
using BuyMeCofee.Maui.Enums;
|
||||||
|
using BuyMeCofee.Maui.Models;
|
||||||
|
|
||||||
|
namespace BuyMeCofee.Maui.Helpers;
|
||||||
|
|
||||||
|
public static class BmcThemeResolver
|
||||||
|
{
|
||||||
|
public static BmcThemeInfo Resolve(BmcButtonTheme theme) => theme switch
|
||||||
|
{
|
||||||
|
BmcButtonTheme.Yellow => new(Color.FromArgb(BmcColors.YellowBg), Color.FromArgb(BmcColors.BrandDark), Colors.Transparent),
|
||||||
|
BmcButtonTheme.Black => new(Color.FromArgb(BmcColors.BlackBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.White => new(Color.FromArgb(BmcColors.WhiteBg), Color.FromArgb(BmcColors.BrandDark), Color.FromArgb(BmcColors.WhiteStroke)),
|
||||||
|
BmcButtonTheme.Blue => new(Color.FromArgb(BmcColors.BlueBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.Violet => new(Color.FromArgb(BmcColors.VioletBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.Orange => new(Color.FromArgb(BmcColors.OrangeBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.Red => new(Color.FromArgb(BmcColors.RedBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.Green => new(Color.FromArgb(BmcColors.GreenBg), Colors.White, Colors.Transparent),
|
||||||
|
BmcButtonTheme.Custom => throw new InvalidOperationException(
|
||||||
|
"Custom theme requires setting CustomBackgroundColor and CustomTextColor properties directly."),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(theme))
|
||||||
|
};
|
||||||
|
}
|
||||||
3
BuyMeCofee.Maui/Models/BmcThemeInfo.cs
Normal file
3
BuyMeCofee.Maui/Models/BmcThemeInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Models;
|
||||||
|
|
||||||
|
public record BmcThemeInfo(Color Background, Color TextColor, Color StrokeColor);
|
||||||
10
BuyMeCofee.Maui/Models/SupportRequestedEventArgs.cs
Normal file
10
BuyMeCofee.Maui/Models/SupportRequestedEventArgs.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace BuyMeCofee.Maui.Models;
|
||||||
|
|
||||||
|
public class SupportRequestedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public string Username { get; init; } = string.Empty;
|
||||||
|
public int Amount { get; init; }
|
||||||
|
public string? Name { get; init; }
|
||||||
|
public string? Message { get; init; }
|
||||||
|
public bool IsMonthly { get; init; }
|
||||||
|
}
|
||||||
BIN
BuyMeCofee.Maui/Resources/Images/bmc_logo.png
Normal file
BIN
BuyMeCofee.Maui/Resources/Images/bmc_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
3
bmc.maui.slnx
Normal file
3
bmc.maui.slnx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="BuyMeCofee.Maui/BuyMeCofee.Maui.csproj" />
|
||||||
|
</Solution>
|
||||||
Reference in New Issue
Block a user