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); public static readonly BindableProperty SupporterEmailProperty = BindableProperty.Create(nameof(SupporterEmail), typeof(string), typeof(BuyMeACoffeeQrCode), null, propertyChanged: OnSupporterPropertyChanged); public static readonly BindableProperty HideIfSupporterProperty = BindableProperty.Create(nameof(HideIfSupporter), typeof(bool), typeof(BuyMeACoffeeQrCode), false, propertyChanged: OnSupporterPropertyChanged); #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); } /// /// Email address to check against your BMC supporters list. /// Requires AccessToken configured via UseBuyMeACoffee(options => ...). /// public string? SupporterEmail { get => (string?)GetValue(SupporterEmailProperty); set => SetValue(SupporterEmailProperty, value); } /// /// When true, the control auto-hides if SupporterEmail is a verified supporter. Default: false. /// public bool HideIfSupporter { get => (bool)GetValue(HideIfSupporterProperty); set => SetValue(HideIfSupporterProperty, 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(); if (stream is null) { _logoBitmap = null; return; } _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(); } } private static void OnSupporterPropertyChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeQrCode qr) BmcSupporterCheck.CheckAndHide(qr, qr.SupporterEmail, qr.HideIfSupporter); } #endregion }