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:
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 |
Reference in New Issue
Block a user