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