shortcut_tile.dart 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  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. semantics.write('. Tap to open.');
  34. if (onLongPress != null) semantics.write(' Long press to change.');
  35. return Pressable(
  36. onTap: onTap,
  37. onLongPress: onLongPress,
  38. semanticLabel: semantics.toString(),
  39. child: Column(
  40. mainAxisSize: MainAxisSize.min,
  41. children: [
  42. Container(
  43. width: size,
  44. height: size,
  45. decoration: BoxDecoration(
  46. color: colors.surface,
  47. borderRadius: BorderRadius.circular(size * 0.17),
  48. border: Border.all(
  49. color: placeholder
  50. ? colors.accent.withValues(alpha: 0.16)
  51. : colors.border,
  52. ),
  53. boxShadow: [
  54. BoxShadow(
  55. color: colors.shadow.withValues(
  56. alpha: Theme.of(context).brightness == Brightness.dark
  57. ? 0.2
  58. : 0.04,
  59. ),
  60. blurRadius: 26,
  61. offset: const Offset(0, 10),
  62. ),
  63. ],
  64. ),
  65. child: Stack(
  66. children: [
  67. Center(
  68. child: AppIcon(
  69. iconAsset,
  70. size: iconSize,
  71. color: placeholder ? colors.inkMuted : colors.ink,
  72. ),
  73. ),
  74. if (placeholder)
  75. Positioned(
  76. top: size * 0.09,
  77. right: size * 0.09,
  78. child: Container(
  79. width: 26,
  80. height: 26,
  81. decoration: BoxDecoration(
  82. color: colors.accent,
  83. shape: BoxShape.circle,
  84. ),
  85. alignment: Alignment.center,
  86. child: AppIcon(
  87. 'assets/icons/plus.svg',
  88. size: 14,
  89. color: colors.accentInk,
  90. ),
  91. ),
  92. ),
  93. ],
  94. ),
  95. ),
  96. const SizedBox(height: 12),
  97. Text(
  98. title,
  99. maxLines: 1,
  100. overflow: TextOverflow.ellipsis,
  101. textAlign: TextAlign.center,
  102. style: theme.textTheme.titleMedium?.copyWith(
  103. fontSize: size >= 160 ? 16 : 14,
  104. ),
  105. ),
  106. if (subtitle != null) ...[
  107. const SizedBox(height: 2),
  108. SizedBox(
  109. height: 17,
  110. child: Text(
  111. subtitle!,
  112. maxLines: 1,
  113. overflow: TextOverflow.ellipsis,
  114. textAlign: TextAlign.center,
  115. style: theme.textTheme.bodySmall?.copyWith(
  116. color: placeholder ? colors.accent : colors.inkMuted,
  117. fontWeight: placeholder ? FontWeight.w600 : FontWeight.w400,
  118. fontSize: size >= 160 ? 12 : 11,
  119. ),
  120. ),
  121. ),
  122. ],
  123. ],
  124. ),
  125. );
  126. }
  127. }