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
|
// Show notification with Twitch's rejection message
|
||||||
if (parsedIRCMessage.message != null) {
|
if (parsedIRCMessage.message != null) {
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
updateNotification(parsedIRCMessage.message!);
|
updateNotification(parsedIRCMessage.message!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1138,6 +1139,7 @@ abstract class ChatStoreBase with Store {
|
||||||
|
|
||||||
// Prevent double-sending while waiting for server acknowledgment
|
// Prevent double-sending while waiting for server acknowledgment
|
||||||
if (_isWaitingForAck) {
|
if (_isWaitingForAck) {
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
updateNotification('Please wait, sending previous message...');
|
updateNotification('Please wait, sending previous message...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1173,6 +1175,7 @@ abstract class ChatStoreBase with Store {
|
||||||
if (_isWaitingForAck) {
|
if (_isWaitingForAck) {
|
||||||
_isWaitingForAck = false;
|
_isWaitingForAck = false;
|
||||||
toSend = null;
|
toSend = null;
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
updateNotification('Message may not have been sent. Please try again.');
|
updateNotification('Message may not have been sent. Please try again.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1247,9 +1250,6 @@ abstract class ChatStoreBase with Store {
|
||||||
// when copying messages repeatedly.
|
// when copying messages repeatedly.
|
||||||
_notificationTimer?.cancel();
|
_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.
|
// Set the new notification message and create a new timer that will dismiss it after 3 seconds.
|
||||||
_notification = notificationMessage;
|
_notification = notificationMessage;
|
||||||
_notificationTimer = Timer(
|
_notificationTimer = Timer(
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,12 @@ class _HomeState extends State<Home> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
selectedIndex: _homeStore.selectedIndex,
|
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: () {
|
onLongPress: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.mediumImpact();
|
||||||
|
|
||||||
showModalBottomSheetWithProperFocus(
|
showModalBottomSheetWithProperFocus(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,17 @@ class StreamCard extends StatelessWidget {
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onLongPress: () => showDialog(
|
onLongPress: () {
|
||||||
context: context,
|
HapticFeedback.lightImpact();
|
||||||
builder: (context) => FrostyPhotoViewDialog(
|
showDialog(
|
||||||
imageUrl: streamInfo
|
context: context,
|
||||||
.thumbnailUrl, // Pass original URL with {width}x{height} placeholder
|
builder: (context) => FrostyPhotoViewDialog(
|
||||||
cacheKey: cacheKey,
|
imageUrl: streamInfo
|
||||||
),
|
.thumbnailUrl, // Pass original URL with {width}x{height} placeholder
|
||||||
),
|
cacheKey: cacheKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: thumbnail,
|
child: thumbnail,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class _OtherSettingsState extends State<OtherSettings> {
|
||||||
leading: const Icon(Icons.delete_outline_rounded),
|
leading: const Icon(Icons.delete_outline_rounded),
|
||||||
title: const Text('Clear image cache'),
|
title: const Text('Clear image cache'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
await CustomCacheManager.instance.emptyCache();
|
await CustomCacheManager.instance.emptyCache();
|
||||||
await CustomCacheManager.removeOrphanedCacheFiles();
|
await CustomCacheManager.removeOrphanedCacheFiles();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// A custom-styled adaptive [Slider].
|
/// A custom-styled adaptive [Slider].
|
||||||
class SettingsListSlider extends StatelessWidget {
|
class SettingsListSlider extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String trailing;
|
final String trailing;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
|
|
@ -23,15 +24,37 @@ class SettingsListSlider extends StatelessWidget {
|
||||||
this.divisions,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(title),
|
Text(widget.title),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
trailing,
|
widget.trailing,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFeatures: [FontFeature.tabularFigures()],
|
fontFeatures: [FontFeature.tabularFigures()],
|
||||||
),
|
),
|
||||||
|
|
@ -45,13 +68,14 @@ class SettingsListSlider extends StatelessWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Slider.adaptive(
|
Slider.adaptive(
|
||||||
value: value,
|
value: widget.value,
|
||||||
min: min,
|
min: widget.min,
|
||||||
max: max,
|
max: widget.max,
|
||||||
divisions: divisions,
|
divisions: widget.divisions,
|
||||||
onChanged: onChanged,
|
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/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// A custom-styled adaptive [SwitchListTile].
|
/// A custom-styled adaptive [SwitchListTile].
|
||||||
class SettingsListSwitch extends StatelessWidget {
|
class SettingsListSwitch extends StatelessWidget {
|
||||||
|
|
@ -21,7 +22,12 @@ class SettingsListSwitch extends StatelessWidget {
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged != null
|
||||||
|
? (newValue) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
onChanged!(newValue);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class _AccentColorPickerDialogState extends State<AccentColorPickerDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _generateRandomColor() {
|
void _generateRandomColor() {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.lightImpact();
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final newColor = Color.fromARGB(
|
final newColor = Color.fromARGB(
|
||||||
255,
|
255,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:frosty/utils/context_extensions.dart';
|
import 'package:frosty/utils/context_extensions.dart';
|
||||||
|
|
||||||
class ExpandableSectionHeader extends StatelessWidget {
|
class ExpandableSectionHeader extends StatelessWidget {
|
||||||
|
|
@ -24,7 +25,12 @@ class ExpandableSectionHeader extends StatelessWidget {
|
||||||
final isLandscape = context.isLandscape;
|
final isLandscape = context.isLandscape;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onToggle,
|
onTap: onToggle != null
|
||||||
|
? () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
onToggle!();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding:
|
||||||
padding ??
|
padding ??
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue