shortcut_tile.dart 5.2 KB

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