audit and optimize haptic feedback across the app
Add selectionClick to settings switches, discrete slider steps, bottom nav tabs, and expandable section headers. Fix intensity levels for cache clear and random color (medium → light). Add lightImpact to thumbnail long-press. Fix inconsistent search result long-press level. Replace blanket lightImpact in updateNotification with targeted haptics at error/warning call sites to eliminate double-firing and match severity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
21cb79d2c2
commit
02f4cd417b
9 changed files with 70 additions and 26 deletions
|
|
@ -570,6 +570,7 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Show notification with Twitch's rejection message
|
||||
if (parsedIRCMessage.message != null) {
|
||||
HapticFeedback.heavyImpact();
|
||||
updateNotification(parsedIRCMessage.message!);
|
||||
}
|
||||
}
|
||||
|
|
@ -1138,6 +1139,7 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Prevent double-sending while waiting for server acknowledgment
|
||||
if (_isWaitingForAck) {
|
||||
HapticFeedback.mediumImpact();
|
||||
updateNotification('Please wait, sending previous message...');
|
||||
return;
|
||||
}
|
||||
|
|
@ -1173,6 +1175,7 @@ abstract class ChatStoreBase with Store {
|
|||
if (_isWaitingForAck) {
|
||||
_isWaitingForAck = false;
|
||||
toSend = null;
|
||||
HapticFeedback.heavyImpact();
|
||||
updateNotification('Message may not have been sent. Please try again.');
|
||||
}
|
||||
});
|
||||
|
|
@ -1247,9 +1250,6 @@ abstract class ChatStoreBase with Store {
|
|||
// when copying messages repeatedly.
|
||||
_notificationTimer?.cancel();
|
||||
|
||||
// Provide subtle haptic feedback when notification is triggered
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
// Set the new notification message and create a new timer that will dismiss it after 3 seconds.
|
||||
_notification = notificationMessage;
|
||||
_notificationTimer = Timer(
|
||||
|
|
|
|||
|
|
@ -207,7 +207,12 @@ class _HomeState extends State<Home> {
|
|||
),
|
||||
],
|
||||
selectedIndex: _homeStore.selectedIndex,
|
||||
onDestinationSelected: _homeStore.handleTap,
|
||||
onDestinationSelected: (index) {
|
||||
if (index != _homeStore.selectedIndex) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
_homeStore.handleTap(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
),
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
showModalBottomSheetWithProperFocus(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -86,14 +86,17 @@ class StreamCard extends StatelessWidget {
|
|||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onLongPress: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyPhotoViewDialog(
|
||||
imageUrl: streamInfo
|
||||
.thumbnailUrl, // Pass original URL with {width}x{height} placeholder
|
||||
cacheKey: cacheKey,
|
||||
),
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyPhotoViewDialog(
|
||||
imageUrl: streamInfo
|
||||
.thumbnailUrl, // Pass original URL with {width}x{height} placeholder
|
||||
cacheKey: cacheKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: thumbnail,
|
||||
),
|
||||
Container(
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
title: const Text('Clear image cache'),
|
||||
onTap: () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
await CustomCacheManager.instance.emptyCache();
|
||||
await CustomCacheManager.removeOrphanedCacheFiles();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A custom-styled adaptive [Slider].
|
||||
class SettingsListSlider extends StatelessWidget {
|
||||
class SettingsListSlider extends StatefulWidget {
|
||||
final String title;
|
||||
final String trailing;
|
||||
final String? subtitle;
|
||||
|
|
@ -23,15 +24,37 @@ class SettingsListSlider extends StatelessWidget {
|
|||
this.divisions,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsListSlider> createState() => _SettingsListSliderState();
|
||||
}
|
||||
|
||||
class _SettingsListSliderState extends State<SettingsListSlider> {
|
||||
int? _previousStep;
|
||||
|
||||
void _handleChanged(double newValue) {
|
||||
if (widget.divisions != null) {
|
||||
final step =
|
||||
((newValue - widget.min) /
|
||||
(widget.max - widget.min) *
|
||||
widget.divisions!)
|
||||
.round();
|
||||
if (_previousStep != null && step != _previousStep) {
|
||||
HapticFeedback.selectionClick();
|
||||
}
|
||||
_previousStep = step;
|
||||
}
|
||||
widget.onChanged?.call(newValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(title),
|
||||
Text(widget.title),
|
||||
const Spacer(),
|
||||
Text(
|
||||
trailing,
|
||||
widget.trailing,
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
|
|
@ -45,13 +68,14 @@ class SettingsListSlider extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
value: widget.value,
|
||||
min: widget.min,
|
||||
max: widget.max,
|
||||
divisions: widget.divisions,
|
||||
onChanged:
|
||||
widget.onChanged != null ? _handleChanged : null,
|
||||
),
|
||||
if (subtitle != null) Text(subtitle!),
|
||||
if (widget.subtitle != null) Text(widget.subtitle!),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A custom-styled adaptive [SwitchListTile].
|
||||
class SettingsListSwitch extends StatelessWidget {
|
||||
|
|
@ -21,7 +22,12 @@ class SettingsListSwitch extends StatelessWidget {
|
|||
title: Text(title),
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
onChanged: onChanged != null
|
||||
? (newValue) {
|
||||
HapticFeedback.selectionClick();
|
||||
onChanged!(newValue);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class _AccentColorPickerDialogState extends State<AccentColorPickerDialog> {
|
|||
}
|
||||
|
||||
void _generateRandomColor() {
|
||||
HapticFeedback.mediumImpact();
|
||||
HapticFeedback.lightImpact();
|
||||
final random = Random();
|
||||
final newColor = Color.fromARGB(
|
||||
255,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/utils/context_extensions.dart';
|
||||
|
||||
class ExpandableSectionHeader extends StatelessWidget {
|
||||
|
|
@ -24,7 +25,12 @@ class ExpandableSectionHeader extends StatelessWidget {
|
|||
final isLandscape = context.isLandscape;
|
||||
|
||||
return InkWell(
|
||||
onTap: onToggle,
|
||||
onTap: onToggle != null
|
||||
? () {
|
||||
HapticFeedback.selectionClick();
|
||||
onToggle!();
|
||||
}
|
||||
: null,
|
||||
child: Padding(
|
||||
padding:
|
||||
padding ??
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue