Limited
2
0
Files
logikonline 66c8ec8ecf
Some checks failed
Release / Verify Apple Build (push) Successful in 14s
Release / Build & Pack (push) Successful in 8h59m34s
Release / Publish (push) Failing after 16s
feat(ci): add supporter verification and auto-hide functionality
Implement BmcSupporterService to verify supporters via BMC API with configurable caching. Add SupporterEmail and HideIfSupporter properties to all controls to automatically hide donation prompts for existing supporters. Replace PNG logo with SVG for crisp rendering at all scales. Add BmcOptions and BmcConfiguration for library-wide settings. Bump version to 1.1.0.
2026-03-04 00:35:05 -05:00

258 lines
8.1 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);
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
/// <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>
/// Email address to check against your BMC supporters list.
/// Requires AccessToken configured via UseBuyMeACoffee(options => ...).
/// </summary>
public string? SupporterEmail
{
get => (string?)GetValue(SupporterEmailProperty);
set => SetValue(SupporterEmailProperty, value);
}
/// <summary>
/// When true, the control auto-hides if SupporterEmail is a verified supporter. Default: false.
/// </summary>
public bool HideIfSupporter
{
get => (bool)GetValue(HideIfSupporterProperty);
set => SetValue(HideIfSupporterProperty, 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();
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
}