shortcut_tile.dart 4.4 KB

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