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.
220 lines
6.6 KiB
C#
220 lines
6.6 KiB
C#
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
|
|
}
|