feat: donate page - add GitHub Sponsors, custom icons, improved notice card

This commit is contained in:
zarzet
2026-02-07 12:40:46 +07:00
parent c6ee9cda35
commit 03d29a73f7
3 changed files with 397 additions and 29 deletions
+1
View File
@@ -18,4 +18,5 @@ class AppInfo {
static const String kofiUrl = 'https://ko-fi.com/zarzet';
static const String bmacUrl = 'https://buymeacoffee.com/zarzet';
static const String githubSponsorsUrl = 'https://github.com/sponsors/zarzet/';
}
+65 -29
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:spotiflac_android/constants/app_info.dart';
import 'package:spotiflac_android/widgets/donate_icons.dart';
class DonatePage extends StatelessWidget {
const DonatePage({super.key});
@@ -105,28 +106,51 @@ class DonatePage extends StatelessWidget {
const SizedBox(height: 12),
// Notice
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline,
size: 14,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Your name will remain in this version permanently as a token of appreciation. '
'The supporter list is updated manually each month and embedded directly in the app '
'-- no remote server is used. Even if your support period ends, your name stays in '
'every version it was included in.',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: colorScheme.onSurfaceVariant),
Card(
elevation: 0,
color: colorScheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.info_outline_rounded,
size: 20,
color: colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'About Supporters',
style: Theme.of(context).textTheme.titleSmall
?.copyWith(
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 6),
Text(
'Your name will remain in this version permanently as a token of appreciation. '
'The supporter list is updated manually each month and embedded directly in the app '
'-- no remote server is used. Even if your support period ends, your name stays in '
'every version it was included in.',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
),
),
],
@@ -230,7 +254,7 @@ class _DonateLinksCard extends StatelessWidget {
_DonateCardItem(
title: 'Ko-fi',
subtitle: 'ko-fi.com/zarzet',
icon: Icons.coffee_rounded,
customIcon: const KofiIcon(size: 22, color: Colors.white),
color: const Color(0xFFFF5E5B),
url: AppInfo.kofiUrl,
colorScheme: colorScheme,
@@ -245,12 +269,26 @@ class _DonateLinksCard extends StatelessWidget {
_DonateCardItem(
title: 'Buy Me a Coffee',
subtitle: 'buymeacoffee.com/zarzet',
icon: Icons.local_cafe_rounded,
customIcon: const BmacIcon(size: 22, color: Colors.black87),
color: const Color(0xFFFFDD00),
iconColor: Colors.black87,
url: AppInfo.bmacUrl,
colorScheme: colorScheme,
),
Divider(
height: 1,
thickness: 1,
indent: 74,
endIndent: 16,
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
),
_DonateCardItem(
title: 'GitHub Sponsors',
subtitle: 'github.com/sponsors/zarzet',
customIcon: const GitHubIcon(size: 22, color: Colors.white),
color: const Color(0xFF2D333B),
url: AppInfo.githubSponsorsUrl,
colorScheme: colorScheme,
),
],
),
);
@@ -260,20 +298,18 @@ class _DonateLinksCard extends StatelessWidget {
class _DonateCardItem extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Widget customIcon;
final Color color;
final Color? iconColor;
final String url;
final ColorScheme colorScheme;
const _DonateCardItem({
required this.title,
required this.subtitle,
required this.icon,
required this.customIcon,
required this.color,
required this.url,
required this.colorScheme,
this.iconColor,
});
@override
@@ -292,7 +328,7 @@ class _DonateCardItem extends StatelessWidget {
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: iconColor ?? Colors.white, size: 22),
child: Center(child: customIcon),
),
const SizedBox(width: 14),
Expanded(
+331
View File
@@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
/// Custom painted icons for donate platforms
class KofiIcon extends StatelessWidget {
final double size;
final Color color;
const KofiIcon({super.key, this.size = 22, this.color = Colors.white});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size),
painter: _KofiPainter(color),
);
}
}
class _KofiPainter extends CustomPainter {
final Color color;
_KofiPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final s = size.width;
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
// Cup body
final cup = RRect.fromRectAndRadius(
Rect.fromLTWH(s * 0.08, s * 0.28, s * 0.62, s * 0.52),
Radius.circular(s * 0.12),
);
canvas.drawRRect(cup, paint);
// Handle
final handlePaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = s * 0.08
..strokeCap = StrokeCap.round;
final handlePath = Path()
..moveTo(s * 0.70, s * 0.40)
..quadraticBezierTo(s * 0.92, s * 0.40, s * 0.92, s * 0.54)
..quadraticBezierTo(s * 0.92, s * 0.68, s * 0.70, s * 0.68);
canvas.drawPath(handlePath, handlePaint);
// Heart on cup
final heartPaint = Paint()
..color = const Color(0xFFFF5E5B)
..style = PaintingStyle.fill;
final hx = s * 0.39;
final hy = s * 0.46;
final hs = s * 0.12;
final heart = Path()
..moveTo(hx, hy + hs * 0.3)
..cubicTo(hx - hs, hy - hs * 0.3, hx - hs * 0.5, hy - hs, hx, hy - hs * 0.4)
..cubicTo(hx + hs * 0.5, hy - hs, hx + hs, hy - hs * 0.3, hx, hy + hs * 0.3)
..close();
canvas.drawPath(heart, heartPaint);
// Steam lines
final steamPaint = Paint()
..color = color.withValues(alpha: 0.6)
..style = PaintingStyle.stroke
..strokeWidth = s * 0.04
..strokeCap = StrokeCap.round;
for (var i = 0; i < 2; i++) {
final sx = s * (0.30 + i * 0.18);
final steam = Path()
..moveTo(sx, s * 0.24)
..quadraticBezierTo(sx - s * 0.04, s * 0.18, sx, s * 0.12);
canvas.drawPath(steam, steamPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class BmacIcon extends StatelessWidget {
final double size;
final Color color;
const BmacIcon({super.key, this.size = 22, this.color = Colors.black87});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size),
painter: _BmacPainter(color),
);
}
}
class _BmacPainter extends CustomPainter {
final Color color;
_BmacPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final s = size.width;
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
// Cup body (slightly tapered)
final cup = Path()
..moveTo(s * 0.15, s * 0.35)
..lineTo(s * 0.20, s * 0.82)
..quadraticBezierTo(s * 0.20, s * 0.90, s * 0.28, s * 0.90)
..lineTo(s * 0.56, s * 0.90)
..quadraticBezierTo(s * 0.64, s * 0.90, s * 0.64, s * 0.82)
..lineTo(s * 0.69, s * 0.35)
..close();
canvas.drawPath(cup, paint);
// Cup rim
final rim = RRect.fromRectAndRadius(
Rect.fromLTWH(s * 0.10, s * 0.30, s * 0.64, s * 0.10),
Radius.circular(s * 0.05),
);
canvas.drawRRect(rim, paint);
// Handle
final handlePaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = s * 0.07
..strokeCap = StrokeCap.round;
final handle = Path()
..moveTo(s * 0.69, s * 0.42)
..quadraticBezierTo(s * 0.90, s * 0.42, s * 0.90, s * 0.56)
..quadraticBezierTo(s * 0.90, s * 0.70, s * 0.69, s * 0.70);
canvas.drawPath(handle, handlePaint);
// Steam
final steamPaint = Paint()
..color = color.withValues(alpha: 0.5)
..style = PaintingStyle.stroke
..strokeWidth = s * 0.04
..strokeCap = StrokeCap.round;
for (var i = 0; i < 3; i++) {
final sx = s * (0.26 + i * 0.14);
final steam = Path()
..moveTo(sx, s * 0.26)
..quadraticBezierTo(sx + s * 0.03, s * 0.18, sx, s * 0.10);
canvas.drawPath(steam, steamPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class GitHubIcon extends StatelessWidget {
final double size;
final Color color;
const GitHubIcon({super.key, this.size = 22, this.color = Colors.white});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(size, size),
painter: _GitHubPainter(color),
);
}
}
class _GitHubPainter extends CustomPainter {
final Color color;
_GitHubPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final s = size.width;
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
// GitHub octocat silhouette (simplified mark)
// Based on the GitHub logo path, scaled to fit
final scale = s / 24.0;
final path = Path();
// Outer circle/head shape
path.moveTo(12 * scale, 0.5 * scale);
path.cubicTo(
5.37 * scale, 0.5 * scale,
0 * scale, 5.87 * scale,
0 * scale, 12.5 * scale,
);
path.cubicTo(
0 * scale, 17.78 * scale,
3.44 * scale, 22.27 * scale,
8.21 * scale, 23.85 * scale,
);
path.cubicTo(
8.81 * scale, 23.96 * scale,
9.02 * scale, 23.59 * scale,
9.02 * scale, 23.27 * scale,
);
path.cubicTo(
9.02 * scale, 22.98 * scale,
9.01 * scale, 22.01 * scale,
9.01 * scale, 21.01 * scale,
);
// Left arm
path.cubicTo(
5.67 * scale, 21.71 * scale,
4.97 * scale, 19.56 * scale,
4.97 * scale, 19.56 * scale,
);
path.cubicTo(
4.42 * scale, 18.22 * scale,
3.63 * scale, 17.85 * scale,
3.63 * scale, 17.85 * scale,
);
path.cubicTo(
2.55 * scale, 17.12 * scale,
3.71 * scale, 17.13 * scale,
3.71 * scale, 17.13 * scale,
);
path.cubicTo(
4.90 * scale, 17.22 * scale,
5.53 * scale, 18.36 * scale,
5.53 * scale, 18.36 * scale,
);
path.cubicTo(
6.58 * scale, 20.05 * scale,
8.36 * scale, 19.53 * scale,
9.05 * scale, 19.22 * scale,
);
path.cubicTo(
9.16 * scale, 18.45 * scale,
9.47 * scale, 17.93 * scale,
9.81 * scale, 17.63 * scale,
);
// Bottom
path.cubicTo(
7.15 * scale, 17.33 * scale,
4.34 * scale, 16.33 * scale,
4.34 * scale, 11.93 * scale,
);
path.cubicTo(
4.34 * scale, 10.68 * scale,
4.81 * scale, 9.66 * scale,
5.55 * scale, 8.86 * scale,
);
path.cubicTo(
5.43 * scale, 8.56 * scale,
5.02 * scale, 7.40 * scale,
5.67 * scale, 5.82 * scale,
);
path.cubicTo(
5.67 * scale, 5.82 * scale,
6.66 * scale, 5.50 * scale,
8.98 * scale, 6.99 * scale,
);
path.cubicTo(
9.94 * scale, 6.72 * scale,
10.98 * scale, 6.59 * scale,
12.0 * scale, 6.58 * scale,
);
path.cubicTo(
13.02 * scale, 6.59 * scale,
14.06 * scale, 6.72 * scale,
15.02 * scale, 6.99 * scale,
);
path.cubicTo(
17.34 * scale, 5.50 * scale,
18.33 * scale, 5.82 * scale,
18.33 * scale, 5.82 * scale,
);
path.cubicTo(
18.98 * scale, 7.40 * scale,
18.57 * scale, 8.56 * scale,
18.45 * scale, 8.86 * scale,
);
path.cubicTo(
19.19 * scale, 9.66 * scale,
19.66 * scale, 10.68 * scale,
19.66 * scale, 11.93 * scale,
);
path.cubicTo(
19.66 * scale, 16.34 * scale,
16.84 * scale, 17.32 * scale,
14.17 * scale, 17.62 * scale,
);
path.cubicTo(
14.59 * scale, 17.99 * scale,
14.97 * scale, 18.70 * scale,
14.97 * scale, 19.80 * scale,
);
path.cubicTo(
14.97 * scale, 21.40 * scale,
14.95 * scale, 22.67 * scale,
14.95 * scale, 23.27 * scale,
);
path.cubicTo(
14.95 * scale, 23.60 * scale,
15.16 * scale, 23.97 * scale,
15.77 * scale, 23.85 * scale,
);
path.cubicTo(
20.55 * scale, 22.26 * scale,
24.0 * scale, 17.78 * scale,
24.0 * scale, 12.5 * scale,
);
path.cubicTo(
24.0 * scale, 5.87 * scale,
18.63 * scale, 0.5 * scale,
12.0 * scale, 0.5 * scale,
);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}