shortcut_tile.dart 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import 'package:flutter/material.dart';
  2. import '../../../core/theme/app_colors.dart';
  3. import '../../../core/widgets/app_icon.dart';
  4. import '../../../core/widgets/pressable.dart';
  5. /// One large, generic shortcut on the home screen.
  6. class ShortcutTile extends StatelessWidget {
  7. const ShortcutTile({
  8. super.key,
  9. required this.title,
  10. required this.subtitle,
  11. required this.iconAsset,
  12. required this.size,
  13. required this.assigned,
  14. required this.onTap,
  15. this.onLongPress,
  16. });
  17. final String title;
  18. final String? subtitle;
  19. final String iconAsset;
  20. final double size;
  21. final bool assigned;
  22. final VoidCallback onTap;
  23. final VoidCallback? onLongPress;
  24. @override
  25. Widget build(BuildContext context) {
  26. final colors = context.colors;
  27. final theme = Theme.of(context);
  28. final placeholder = !assigned;
  29. final iconSize = (size * 0.27).clamp(30.0, 46.0);
  30. final semantics = StringBuffer(title);
  31. if (subtitle != null && subtitle!.isNotEmpty) {
  32. semantics.write(', $subtitle');
  33. }
  34. semantics.write('. Tap to open.');
  35. if (onLongPress != null) {
  36. semantics.write(' Long press to change.');
  37. }
  38. return Pressable(
  39. onTap: onTap,
  40. onLongPress: onLongPress,
  41. semanticLabel: semantics.toString(),
  42. child: Column(
  43. mainAxisSize: MainAxisSize.min,
  44. children: [
  45. Container(
  46. width: size,
  47. height: size,
  48. decoration: BoxDecoration(
  49. color: colors.surface,
  50. borderRadius: BorderRadius.circular(size * 0.17),
  51. border: Border.all(
  52. color: placeholder
  53. ? colors.accent.withValues(alpha: 0.16)
  54. : colors.border,
  55. ),
  56. boxShadow: [
  57. BoxShadow(
  58. color: colors.shadow.withValues(
  59. alpha: Theme.of(context).brightness == Brightness.dark
  60. ? 0.2
  61. : 0.04,
  62. ),
  63. blurRadius: 26,
  64. offset: const Offset(0, 10),
  65. ),
  66. ],
  67. ),
  68. child: Stack(
  69. children: [
  70. Center(
  71. child: AppIcon(
  72. iconAsset,
  73. size: iconSize,
  74. color: placeholder ? colors.inkMuted : colors.ink,
  75. ),
  76. ),
  77. if (placeholder)
  78. Positioned(
  79. top: size * 0.09,
  80. right: size * 0.09,
  81. child: Container(
  82. width: 26,
  83. height: 26,
  84. decoration: BoxDecoration(
  85. color: colors.accent,
  86. shape: BoxShape.circle,
  87. ),
  88. alignment: Alignment.center,
  89. child: AppIcon(
  90. 'assets/icons/plus.svg',
  91. size: 14,
  92. color: colors.accentInk,
  93. ),
  94. ),
  95. ),
  96. ],
  97. ),
  98. ),
  99. const SizedBox(height: 12),
  100. Text(
  101. title,
  102. maxLines: 1,
  103. overflow: TextOverflow.ellipsis,
  104. textAlign: TextAlign.center,
  105. style: theme.textTheme.titleMedium?.copyWith(
  106. fontSize: size >= 160 ? 16 : 14,
  107. ),
  108. ),
  109. if (subtitle != null) ...[
  110. const SizedBox(height: 2),
  111. SizedBox(
  112. height: 17,
  113. child: Text(
  114. subtitle!,
  115. maxLines: 1,
  116. overflow: TextOverflow.ellipsis,
  117. textAlign: TextAlign.center,
  118. style: theme.textTheme.bodySmall?.copyWith(
  119. color: placeholder ? colors.accent : colors.inkMuted,
  120. fontWeight: placeholder ? FontWeight.w600 : FontWeight.w400,
  121. fontSize: size >= 160 ? 12 : 11,
  122. ),
  123. ),
  124. ),
  125. ],
  126. ],
  127. ),
  128. );
  129. }
  130. }