Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions JournalApp/Components/DataPointView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject DataPointService DataPointService
@inject HapticFeedbackService HapticFeedback

@if (Point.Type == PointType.Mood)
{
Expand All @@ -14,17 +15,17 @@ else if (Point.Type == PointType.Sleep)
<div class="sleep-controls">
<MudText Class="sleep-hours" Typo="Typo.caption">@((Point.SleepHours ?? 0).ToString("0.0"))</MudText>
<MudIconButton Class="less-sleep" Icon="@Icons.Material.Rounded.Remove" aria-label="Less sleep" OnClick="DecrementSleep" Disabled="@(Point.SleepHours <= 0)" Size="Size.Small" />
<MudSlider T="decimal" @bind-NullableValue="Point.SleepHours" Variant="Variant.Filled" Min="0" Max="24" Step="0.5m" />
<MudSlider T="decimal" NullableValue="Point.SleepHours" NullableValueChanged="SleepChanged" Variant="Variant.Filled" Min="0" Max="24" Step="0.5m" />
<MudIconButton Class="more-sleep" Icon="@Icons.Material.Rounded.Add" aria-label="More sleep" OnClick="IncrementSleep" Disabled="@(Point.SleepHours >= 24)" Size="Size.Small" />
</div>
}
else if (Point.Type == PointType.Scale)
{
<MudRating @bind-SelectedValue="ScaleIndexForMudRating" FullIcon="@Icons.Material.Rounded.Circle" EmptyIcon="@Icons.Material.Rounded.Circle" Color="Color.Primary" />
<MudRating SelectedValue="DataPointService.GetScaleIndex(Point)" SelectedValueChanged="ScaleValueChanged" FullIcon="@Icons.Material.Rounded.Circle" EmptyIcon="@Icons.Material.Rounded.Circle" Color="Color.Primary" />
}
else if (Point.Type == PointType.LowToHigh)
{
<MudToggleGroup T="int?" @bind-Value="Point.ScaleIndex" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleGroup T="int?" Value="Point.ScaleIndex" ValueChanged="ScaleIndexChanged" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleItem T="int?" Value="0">None</MudToggleItem>
<MudToggleItem T="int?" Value="1">Low</MudToggleItem>
<MudToggleItem T="int?" Value="3">Medium</MudToggleItem>
Expand All @@ -33,7 +34,7 @@ else if (Point.Type == PointType.LowToHigh)
}
else if (Point.Type == PointType.MildToSevere)
{
<MudToggleGroup T="int?" @bind-Value="Point.ScaleIndex" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleGroup T="int?" Value="Point.ScaleIndex" ValueChanged="ScaleIndexChanged" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleItem T="int?" Value="0">None</MudToggleItem>
<MudToggleItem T="int?" Value="1">Mild</MudToggleItem>
<MudToggleItem T="int?" Value="3">Moderate</MudToggleItem>
Expand All @@ -42,7 +43,7 @@ else if (Point.Type == PointType.MildToSevere)
}
else if (Point.Type == PointType.Bool)
{
<MudToggleGroup T="bool?" @bind-Value="Point.Bool" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleGroup T="bool?" Value="Point.Bool" ValueChanged="OnBoolChanged" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleItem T="bool?" Value="false">No</MudToggleItem>
<MudToggleItem T="bool?" Value="true">Yes</MudToggleItem>
</MudToggleGroup>
Expand Down Expand Up @@ -74,7 +75,7 @@ else if (Point.Type == PointType.Note)
else if (Point.Type == PointType.Medication)
{
<div class="medication-controls">
<MudToggleGroup T="bool?" @bind-Value="Point.Bool" @bind-Value:after="OnMedicationTakenChanged" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleGroup T="bool?" Value="Point.Bool" ValueChanged="OnMedicationTakenChanged" SelectionMode="SelectionMode.ToggleSelection" Size="Size.Small" CheckMark>
<MudToggleItem T="bool?" Value="false">No</MudToggleItem>
<MudToggleItem T="bool?" Value="true">Yes</MudToggleItem>
</MudToggleGroup>
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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();
}

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions JournalApp/Components/EditCategoryDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@inject ILogger<EditCategoryDialog> logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
@inject HapticFeedbackService HapticFeedback

<MudDialog Class="category-dialog" DefaultFocus="DefaultFocus.FirstChild" OnBackdropClick="Submit">
<TitleContent>
Expand Down Expand Up @@ -121,6 +122,7 @@

Category.Deleted = true;
logger.LogDebug($"Deleted {Category}");
HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, Category);
}

Expand All @@ -133,6 +135,7 @@
if (!_form.IsValid)
{
logger.LogDebug("Form was not valid");
HapticFeedback.Reject();

if (Category == null)
Cancel();
Expand All @@ -151,6 +154,7 @@

logger.LogDebug($"Created new category <Name: {Category.Name}, Type: {Category.Type}>");

HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, Category);
return true;
}
Expand Down
2 changes: 2 additions & 0 deletions JournalApp/Components/EditDoseDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@inject ILogger<EditDoseDialog> logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
@inject HapticFeedbackService HapticFeedback

<MudDialog DefaultFocus="DefaultFocus.FirstChild" OnBackdropClick="Submit">
<TitleContent>
Expand Down Expand Up @@ -84,6 +85,7 @@
logger.LogDebug("Updating with new dose and marking as taken");
}

HapticFeedback.Confirm();
KeyEventService.CloseDialog(MudDialog, true);
}

Expand Down
2 changes: 2 additions & 0 deletions JournalApp/Components/EditNoteDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@inject ILogger<EditNoteDialog> logger
@inject IDialogService DialogService
@inject KeyEventService KeyEventService
@inject HapticFeedbackService HapticFeedback

<MudDialog DefaultFocus="DefaultFocus.FirstChild" OnBackdropClick="Submit">
<TitleContent>
Expand Down Expand Up @@ -80,6 +81,7 @@
else
{
logger.LogInformation("Submitting note");
HapticFeedback.Confirm();
}

KeyEventService.CloseDialog(MudDialog, true);
Expand Down
7 changes: 6 additions & 1 deletion JournalApp/Components/EmojiPicker.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@namespace JournalApp

<EmojiButton OnClick="@(() => _isOpen = !_isOpen)"
<EmojiButton OnClick="TogglePicker"
AriaLabel="Choose emoji for day's mood">
@(SelectedMood ?? DataPoint.Moods[0])
</EmojiButton>
Expand Down Expand Up @@ -33,6 +33,11 @@
[Parameter]
public EventCallback<string> OnMoodSelected { get; set; }

private void TogglePicker()
{
_isOpen = !_isOpen;
}

private async Task OnMoodClicked(string mood)
{
_isOpen = false;
Expand Down
1 change: 1 addition & 0 deletions JournalApp/Data/CommonServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static void AddCommonJournalAppServices(this IServiceCollection services)

services.AddSingleton<AppDataService>();
services.AddSingleton<AppDbSeeder>();
services.AddSingleton<HapticFeedbackService>();
services.AddSingleton<KeyEventService>();
services.AddSingleton<PreferenceService>();
services.AddSingleton<CalendarService>();
Expand Down
76 changes: 76 additions & 0 deletions JournalApp/Data/HapticFeedbackService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace JournalApp;

/// <summary>
/// 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.
/// </summary>
public sealed class HapticFeedbackService
{
/// <summary>
/// Signals an action completed successfully, like submitting a dialog.
/// </summary>
public void Confirm()
{
#if ANDROID
// Confirm/Reject exist since API 30.
Perform(OperatingSystem.IsAndroidVersionAtLeast(30) ? Android.Views.FeedbackConstants.Confirm : Android.Views.FeedbackConstants.KeyboardTap);
#endif
}

/// <summary>
/// Signals an action was denied, like failing validation.
/// </summary>
public void Reject()
{
#if ANDROID
Perform(OperatingSystem.IsAndroidVersionAtLeast(30) ? Android.Views.FeedbackConstants.Reject : Android.Views.FeedbackConstants.LongPress);
#endif
}

/// <summary>
/// A switch or binary choice turning on.
/// </summary>
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
}

/// <summary>
/// A switch or binary choice turning off.
/// </summary>
public void ToggleOff()
{
#if ANDROID
Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.ToggleOff : Android.Views.FeedbackConstants.ClockTick);
#endif
}

/// <summary>
/// A discrete step through segmented values, like picking a mood or nudging sleep hours.
/// </summary>
public void Tick()
{
#if ANDROID
Perform(OperatingSystem.IsAndroidVersionAtLeast(34) ? Android.Views.FeedbackConstants.SegmentTick : Android.Views.FeedbackConstants.ClockTick);
#endif
}

/// <summary>
/// A very soft tick for continuously dragging through many values, like a slider.
/// </summary>
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
}
7 changes: 7 additions & 0 deletions JournalApp/Pages/ManageCategoriesPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@implements IDisposable
@inject ILogger<ManageCategoriesPage> logger
@inject IDbContextFactory<AppDbContext> DbFactory
@inject HapticFeedbackService HapticFeedback

<header class="page-header">
<div class="page-toolbar">
Expand Down Expand Up @@ -74,6 +75,12 @@
{
logger.LogInformation($"Toggled category enabled <{c}, {newValue}>");
c.Enabled = newValue;

if (newValue)
HapticFeedback.ToggleOn();
else
HapticFeedback.ToggleOff();

await db.SaveChangesAsync();
}

Expand Down
28 changes: 25 additions & 3 deletions JournalApp/Pages/SettingsPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@inject ILogger<SettingsPage> logger
@inject AppDataUIService AppDataUIService
@inject IFilePicker FilePicker
@inject HapticFeedbackService HapticFeedback

@if (_busy)
{
Expand Down Expand Up @@ -32,20 +33,20 @@ else
<div class="settings-item">
<MudText Class="settings-item-title">Theme</MudText>

<MudToggleGroup T="AppTheme" Value="PreferenceService.SelectedAppTheme" ValueChanged="v => PreferenceService.SelectedAppTheme = v" SelectionMode="SelectionMode.SingleSelection" Size="Size.Small" CheckMark aria-label="Theme for entire app">
<MudToggleGroup T="AppTheme" Value="PreferenceService.SelectedAppTheme" ValueChanged="SelectedAppThemeChanged" SelectionMode="SelectionMode.SingleSelection" Size="Size.Small" CheckMark aria-label="Theme for entire app">
<MudToggleItem T="AppTheme" Value="@(AppTheme.Light)">Light</MudToggleItem>
<MudToggleItem T="AppTheme" Value="@(AppTheme.Dark)">Dark</MudToggleItem>
<MudToggleItem T="AppTheme" Value="@(AppTheme.Unspecified)">System</MudToggleItem>
</MudToggleGroup>
</div>

<div class="settings-item">
<MudSwitch T="bool" Value="PreferenceService.SupportsDeviceColors && PreferenceService.UseDeviceColors" ValueChanged="v => PreferenceService.UseDeviceColors = v"
<MudSwitch T="bool" Value="PreferenceService.SupportsDeviceColors && PreferenceService.UseDeviceColors" ValueChanged="UseDeviceColorsChanged"
Disabled="!PreferenceService.SupportsDeviceColors" Color="Color.Primary" aria-label="Color the app from your device wallpaper">Use device colors</MudSwitch>
</div>

<div class="settings-item">
<MudSwitch @bind-Value="PreferenceService.HideNotes" Color="Color.Primary" aria-label="Hide the notes section on the home page">Hide "Today's notes"</MudSwitch>
<MudSwitch T="bool" Value="PreferenceService.HideNotes" ValueChanged="HideNotesChanged" Color="Color.Primary" aria-label="Hide the notes section on the home page">Hide "Today's notes"</MudSwitch>
</div>
</div>
</section>
Expand Down Expand Up @@ -151,6 +152,27 @@ else
void SelectedAppThemeChanged(AppTheme newTheme)
{
PreferenceService.SelectedAppTheme = newTheme;
HapticFeedback.Tick();
}

void UseDeviceColorsChanged(bool newValue)
{
PreferenceService.UseDeviceColors = newValue;

if (newValue)
HapticFeedback.ToggleOn();
else
HapticFeedback.ToggleOff();
}

void HideNotesChanged(bool newValue)
{
PreferenceService.HideNotes = newValue;

if (newValue)
HapticFeedback.ToggleOn();
else
HapticFeedback.ToggleOff();
}

void SetUpSafetyPlan()
Expand Down
Loading