diff --git a/JournalApp/Components/DataPointView.razor b/JournalApp/Components/DataPointView.razor
index 1702447..7718788 100644
--- a/JournalApp/Components/DataPointView.razor
+++ b/JournalApp/Components/DataPointView.razor
@@ -4,6 +4,7 @@
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject DataPointService DataPointService
+@inject HapticFeedbackService HapticFeedback
@if (Point.Type == PointType.Mood)
{
@@ -14,17 +15,17 @@ else if (Point.Type == PointType.Sleep)
@((Point.SleepHours ?? 0).ToString("0.0"))
-
+
}
else if (Point.Type == PointType.Scale)
{
-
+
}
else if (Point.Type == PointType.LowToHigh)
{
-
+
None
Low
Medium
@@ -33,7 +34,7 @@ else if (Point.Type == PointType.LowToHigh)
}
else if (Point.Type == PointType.MildToSevere)
{
-
+
None
Mild
Moderate
@@ -42,7 +43,7 @@ else if (Point.Type == PointType.MildToSevere)
}
else if (Point.Type == PointType.Bool)
{
-
+
No
Yes
@@ -74,7 +75,7 @@ else if (Point.Type == PointType.Note)
else if (Point.Type == PointType.Medication)
{
-
+
No
Yes
@@ -93,12 +94,6 @@ else if (Point.Type == PointType.Medication)
[Parameter]
public EventCallback StateChanged { get; set; }
- public int ScaleIndexForMudRating
- {
- get => DataPointService.GetScaleIndex(Point);
- set => DataPointService.SetScaleIndex(Point, value);
- }
-
string NoteLabel
{
get
@@ -121,12 +116,43 @@ else if (Point.Type == PointType.Medication)
{
logger.LogDebug("Decrementing sleep");
DataPointService.DecrementSleep(Point);
+ HapticFeedback.Tick();
}
void IncrementSleep()
{
logger.LogDebug("Incrementing sleep");
DataPointService.IncrementSleep(Point);
+ HapticFeedback.Tick();
+ }
+
+ void SleepChanged(decimal? value)
+ {
+ Point.SleepHours = value;
+ // Dragging crosses many half-hour steps in quick succession so use the soft frequent tick.
+ HapticFeedback.DragTick();
+ }
+
+ void ScaleValueChanged(int value)
+ {
+ DataPointService.SetScaleIndex(Point, value);
+ HapticFeedback.Tick();
+ }
+
+ void ScaleIndexChanged(int? value)
+ {
+ Point.ScaleIndex = value;
+ HapticFeedback.Tick();
+ }
+
+ void OnBoolChanged(bool? value)
+ {
+ Point.Bool = value;
+
+ if (value == true)
+ HapticFeedback.ToggleOn();
+ else
+ HapticFeedback.ToggleOff();
}
async Task EditTextInDialog()
@@ -138,11 +164,18 @@ else if (Point.Type == PointType.Medication)
await StateChanged.InvokeAsync();
}
- async Task OnMedicationTakenChanged()
+ async Task OnMedicationTakenChanged(bool? value)
{
+ Point.Bool = value;
+
// Use service to handle medication dose reset logic
DataPointService.HandleMedicationTakenChanged(Point);
+ if (value == true)
+ HapticFeedback.ToggleOn();
+ else
+ HapticFeedback.ToggleOff();
+
await StateChanged.InvokeAsync();
}
@@ -159,6 +192,7 @@ else if (Point.Type == PointType.Medication)
{
logger.LogDebug("Mood selected: {mood}", mood);
DataPointService.SetMood(Point, mood);
+ HapticFeedback.Tick();
// Show a motivational quote when the sob emoji is selected
if (mood == DataPoint.Moods[^1]) // Sob emoji (ðŸ˜) is the last mood in the list
diff --git a/JournalApp/Components/EditCategoryDialog.razor b/JournalApp/Components/EditCategoryDialog.razor
index eb219f0..9f9a82f 100644
--- a/JournalApp/Components/EditCategoryDialog.razor
+++ b/JournalApp/Components/EditCategoryDialog.razor
@@ -2,6 +2,7 @@
@inject ILogger logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
+@inject HapticFeedbackService HapticFeedback
@@ -121,6 +122,7 @@
Category.Deleted = true;
logger.LogDebug($"Deleted {Category}");
+ HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, Category);
}
@@ -133,6 +135,7 @@
if (!_form.IsValid)
{
logger.LogDebug("Form was not valid");
+ HapticFeedback.Reject();
if (Category == null)
Cancel();
@@ -151,6 +154,7 @@
logger.LogDebug($"Created new category ");
+ HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, Category);
return true;
}
diff --git a/JournalApp/Components/EditDoseDialog.razor b/JournalApp/Components/EditDoseDialog.razor
index f82a5e8..ba3475b 100644
--- a/JournalApp/Components/EditDoseDialog.razor
+++ b/JournalApp/Components/EditDoseDialog.razor
@@ -2,6 +2,7 @@
@inject ILogger logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
+@inject HapticFeedbackService HapticFeedback
@@ -84,6 +85,7 @@
logger.LogDebug("Updating with new dose and marking as taken");
}
+ HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, true);
}
diff --git a/JournalApp/Components/EditNoteDialog.razor b/JournalApp/Components/EditNoteDialog.razor
index 6fa9ea3..470238f 100644
--- a/JournalApp/Components/EditNoteDialog.razor
+++ b/JournalApp/Components/EditNoteDialog.razor
@@ -2,6 +2,7 @@
@inject ILogger logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
+@inject HapticFeedbackService HapticFeedback
@@ -80,6 +81,7 @@
else
{
logger.LogInformation("Submitting note");
+ HapticFeedback.Confirm();
}
KeyEventService.CloseDialog(MudDialog, true);
diff --git a/JournalApp/Components/EmojiPicker.razor b/JournalApp/Components/EmojiPicker.razor
index 9c5d882..484c615 100644
--- a/JournalApp/Components/EmojiPicker.razor
+++ b/JournalApp/Components/EmojiPicker.razor
@@ -1,6 +1,6 @@
@namespace JournalApp
-
@(SelectedMood ?? DataPoint.Moods[0])
@@ -33,6 +33,11 @@
[Parameter]
public EventCallback OnMoodSelected { get; set; }
+ private void TogglePicker()
+ {
+ _isOpen = !_isOpen;
+ }
+
private async Task OnMoodClicked(string mood)
{
_isOpen = false;
diff --git a/JournalApp/Data/CommonServices.cs b/JournalApp/Data/CommonServices.cs
index 7bf80da..2e5e8bf 100644
--- a/JournalApp/Data/CommonServices.cs
+++ b/JournalApp/Data/CommonServices.cs
@@ -22,6 +22,7 @@ public static void AddCommonJournalAppServices(this IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/JournalApp/Data/HapticFeedbackService.cs b/JournalApp/Data/HapticFeedbackService.cs
new file mode 100644
index 0000000..57fb5f2
--- /dev/null
+++ b/JournalApp/Data/HapticFeedbackService.cs
@@ -0,0 +1,76 @@
+namespace JournalApp;
+
+///
+/// Plays semantic haptic cues for key interactions.
+/// On Android this goes through the decor view's PerformHapticFeedback, which needs no VIBRATE permission and respects the system touch feedback setting; elsewhere it's a no-op.
+///
+public sealed class HapticFeedbackService
+{
+ ///
+ /// Signals an action completed successfully, like submitting a dialog.
+ ///
+ public void Confirm()
+ {
+#if ANDROID
+ // Confirm/Reject exist since API 30.
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(30) ? Android.Views.FeedbackConstants.Confirm : Android.Views.FeedbackConstants.KeyboardTap);
+#endif
+ }
+
+ ///
+ /// Signals an action was denied, like failing validation.
+ ///
+ public void Reject()
+ {
+#if ANDROID
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(30) ? Android.Views.FeedbackConstants.Reject : Android.Views.FeedbackConstants.LongPress);
+#endif
+ }
+
+ ///
+ /// A switch or binary choice turning on.
+ ///
+ public void ToggleOn()
+ {
+#if ANDROID
+ // ToggleOn/ToggleOff and SegmentTick exist since API 34.
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.ToggleOn : Android.Views.FeedbackConstants.ClockTick);
+#endif
+ }
+
+ ///
+ /// A switch or binary choice turning off.
+ ///
+ public void ToggleOff()
+ {
+#if ANDROID
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.ToggleOff : Android.Views.FeedbackConstants.ClockTick);
+#endif
+ }
+
+ ///
+ /// A discrete step through segmented values, like picking a mood or nudging sleep hours.
+ ///
+ public void Tick()
+ {
+#if ANDROID
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.SegmentTick : Android.Views.FeedbackConstants.ClockTick);
+#endif
+ }
+
+ ///
+ /// A very soft tick for continuously dragging through many values, like a slider.
+ ///
+ public void DragTick()
+ {
+#if ANDROID
+ // SegmentFrequentTick exists since API 34 and stays soft when fired rapidly during a drag.
+ Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.SegmentFrequentTick : Android.Views.FeedbackConstants.ClockTick);
+#endif
+ }
+
+#if ANDROID
+ private static void Perform(Android.Views.FeedbackConstants constant) =>
+ Platform.CurrentActivity?.Window?.DecorView?.PerformHapticFeedback(constant);
+#endif
+}
diff --git a/JournalApp/Pages/ManageCategoriesPage.razor b/JournalApp/Pages/ManageCategoriesPage.razor
index c79d538..5d5593e 100644
--- a/JournalApp/Pages/ManageCategoriesPage.razor
+++ b/JournalApp/Pages/ManageCategoriesPage.razor
@@ -4,6 +4,7 @@
@implements IDisposable
@inject ILogger logger
@inject IDbContextFactory DbFactory
+@inject HapticFeedbackService HapticFeedback