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;
///
/// Generates and displays a QR code linking to a Buy Me a Coffee profile,
/// with the BMC coffee cup logo overlaid in the center.
///
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
/// Your Buy Me a Coffee username/slug.
public string Username
{
get => (string)GetValue(UsernameProperty);
set => SetValue(UsernameProperty, value);
}
/// Width and height of the QR code control. Default: 200
public double Size
{
get => (double)GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
/// QR code foreground (module) color. Default: Black
public Color ForegroundColor
{
get => (Color)GetValue(ForegroundColorProperty);
set => SetValue(ForegroundColorProperty, value);
}
/// QR code background color. Default: White
public new Color BackgroundColor
{
get => (Color)GetValue(BackgroundColorProperty);
set => SetValue(BackgroundColorProperty, value);
}
/// Logo size as fraction of QR code size (0.0 - 0.35). Default: 0.25
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
}