Skip to content

Commit 146a627

Browse files
committed
Fix color changes on theme change
1 parent 337e2a4 commit 146a627

File tree

7 files changed

+92
-85
lines changed

7 files changed

+92
-85
lines changed

src/Bible.Alarm/App.xaml.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ private void OnRequestedThemeChanged(object? sender, AppThemeChangedEventArgs e)
152152
UpdateThemeAwareColorResources();
153153
WindowSetupService.UpdateNavigationBarColors();
154154
// Notify ViewModels to update theme-aware bindings (e.g., day button colors)
155-
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage());
155+
// Use BeginInvokeOnMainThread to ensure resources are fully propagated before ViewModels update
156+
MainThread.BeginInvokeOnMainThread(() =>
157+
{
158+
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage());
159+
});
156160
}
157161
}

src/Bible.Alarm/Common/ViewHelpers/Converters/DayBackgroundColorConverter.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
1-
using System.ComponentModel;
21
using System.Globalization;
32
using Bible.Alarm.Shared.Models.Enums;
43
using Bible.Alarm.ViewModels;
54

65
namespace Bible.Alarm.Common.ViewHelpers.Converters;
76

8-
public sealed class DayBackgroundColorConverter : IValueConverter, IMultiValueConverter, INotifyPropertyChanged
7+
public sealed class DayBackgroundColorConverter : IValueConverter, IMultiValueConverter
98
{
10-
public event PropertyChangedEventHandler? PropertyChanged;
11-
12-
public DayBackgroundColorConverter()
13-
{
14-
// Subscribe to theme changes when converter is instantiated
15-
if (Application.Current != null)
16-
{
17-
Application.Current.RequestedThemeChanged += OnRequestedThemeChanged;
18-
}
19-
}
20-
21-
private void OnRequestedThemeChanged(object? sender, AppThemeChangedEventArgs e)
22-
{
23-
// Notify that the converter output has changed, causing all bindings to re-evaluate
24-
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
25-
}
269
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
2710
{
2811
var theme = ThemeColors.GetCurrentTheme();

src/Bible.Alarm/Common/ViewHelpers/Converters/DayColorConverter.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
1-
using System.ComponentModel;
21
using System.Globalization;
32
using Bible.Alarm.Shared.Models.Enums;
43
using Bible.Alarm.ViewModels;
54

65
namespace Bible.Alarm.Common.ViewHelpers.Converters;
76

8-
public sealed class DayColorConverter : IValueConverter, IMultiValueConverter, INotifyPropertyChanged
7+
public sealed class DayColorConverter : IValueConverter, IMultiValueConverter
98
{
10-
public event PropertyChangedEventHandler? PropertyChanged;
11-
12-
public DayColorConverter()
13-
{
14-
// Subscribe to theme changes when converter is instantiated
15-
if (Application.Current != null)
16-
{
17-
Application.Current.RequestedThemeChanged += OnRequestedThemeChanged;
18-
}
19-
}
20-
21-
private void OnRequestedThemeChanged(object? sender, AppThemeChangedEventArgs e)
22-
{
23-
// Notify that the converter output has changed, causing all bindings to re-evaluate
24-
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
25-
}
269
private static DaysOfWeek ParseDayParameter(object parameter)
2710
{
2811
return parameter switch

src/Bible.Alarm/ViewModels/BiblePublications/BibleSelectionViewModelHelpers/BiblePublicationSelectionActionDispatcher.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#nullable enable
22
using Bible.Alarm.Stores.Actions.BiblePublications;
33
using Bible.Alarm.Stores.Models;
4-
using Bible.Alarm.ViewModels.Shared;
54
using IDispatcher = Fluxor.IDispatcher;
65

76
namespace Bible.Alarm.ViewModels.BiblePublications.BibleSelectionViewModelHelpers;
@@ -27,7 +26,7 @@ public void DispatchBiblePublicationSelectionActions(BiblePublicationStateItem b
2726
dispatcher.Dispatch(new TrackSelectedAction(biblePublicationItem));
2827
}
2928

30-
public void DispatchLanguageSelectionActions(BiblePublicationStateItem biblePublicationItem, LanguageListViewItemModel language)
29+
public void DispatchLanguageSelectionActions(BiblePublicationStateItem biblePublicationItem)
3130
{
3231
// Only dispatch TrackSelectedAction - its reducer updates both CurrentSchedule and
3332
// CurrentBiblePublicationSchedule, and its effect syncs all properties.

src/Bible.Alarm/ViewModels/BiblePublications/BibleSelectionViewModelHelpers/BiblePublicationSelectionCommandHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public ICommand CreateSelectLanguageCommand(
197197
var biblePublicationItem = CreateBiblePublicationItemForLanguageSelection(
198198
x, publicationCode, sectionNumber, trackNumber, sectionName, publicationName, trackTitle, currentSchedule);
199199
var actionDispatcher = new BiblePublicationSelectionActionDispatcher(dispatcher);
200-
actionDispatcher.DispatchLanguageSelectionActions(biblePublicationItem, x);
200+
actionDispatcher.DispatchLanguageSelectionActions(biblePublicationItem);
201201
});
202202
}
203203

src/Bible.Alarm/ViewModels/Schedule/ScheduleDetailsContainerViewModel.cs

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
using System.Windows.Input;
44
using AutoMapper;
5+
using Bible.Alarm.Common.Messenger;
56
using Bible.Alarm.Shared.Models.Enums;
67
using Bible.Alarm.Stores;
78
using Bible.Alarm.Stores.Actions.Schedule;
89
using Bible.Alarm.Stores.Models;
910
using CommunityToolkit.Mvvm.ComponentModel;
1011
using CommunityToolkit.Mvvm.Input;
12+
using CommunityToolkit.Mvvm.Messaging;
1113
using Fluxor;
1214
using Serilog;
1315
using IDispatcher = Fluxor.IDispatcher;
@@ -42,6 +44,8 @@ public ScheduleDetailsContainerViewModel(
4244
this.mapper = mapper;
4345

4446
state.StateChanged += OnStateChanged;
47+
// Subscribe to theme changes to update day button colors
48+
WeakReferenceMessenger.Default.Register<ThemeChangedMessage>(this, (r, m) => OnThemeChanged());
4549
InitializeCommands();
4650
InitializeFromState();
4751
}
@@ -114,67 +118,81 @@ private void SignalContainerReady()
114118

115119
private void OnStateChanged(object? sender, EventArgs e)
116120
{
117-
var stateValue = state.Value;
118-
var currentSchedule = stateValue.CurrentSchedule;
119-
120-
// If ContainerReadiness was reset to NotReady but we've already signaled ready, reset our flag
121-
// This handles the case where ViewScheduleAction resets ContainerReadiness after containers signaled ready
122-
if (hasSignaledReady && !stateValue.ContainerReadiness.ScheduleDetails && currentSchedule != null)
121+
// Prevent re-entrant calls to avoid cycles
122+
if (isProcessingStateChange)
123123
{
124-
hasSignaledReady = false;
125-
isReadyActionQueued = false; // Reset queued flag as well
126-
// Re-initialize and signal ready again
127-
InitializeFromState();
128124
return;
129125
}
130126

131-
// If we don't have a scheduleId yet (initial state), initialize when CurrentSchedule is set
132-
// But only if we haven't already signaled ready (prevents infinite loop for new schedules with Id=0)
133-
if (scheduleId == 0 && currentSchedule != null && !hasSignaledReady)
127+
isProcessingStateChange = true;
128+
try
134129
{
135-
InitializeFromState();
136-
return;
137-
}
130+
var stateValue = state.Value;
131+
var currentSchedule = stateValue.CurrentSchedule;
138132

139-
// Reset hasSignaledReady when schedule ID changes to a different positive ID (existing schedule opened)
140-
if (currentSchedule != null && currentSchedule.Id != scheduleId && currentSchedule.Id > 0)
141-
{
142-
hasSignaledReady = false;
143-
isReadyActionQueued = false; // Reset queued flag as well
144-
InitializeFromState();
145-
return;
146-
}
133+
// If ContainerReadiness was reset to NotReady but we've already signaled ready, reset our flag
134+
// This handles the case where ViewScheduleAction resets ContainerReadiness after containers signaled ready
135+
if (hasSignaledReady && !stateValue.ContainerReadiness.ScheduleDetails && currentSchedule != null)
136+
{
137+
hasSignaledReady = false;
138+
isReadyActionQueued = false; // Reset queued flag as well
139+
// Re-initialize and signal ready again
140+
InitializeFromState();
141+
return;
142+
}
147143

148-
// Only update properties if they changed (don't re-initialize)
149-
if (currentSchedule != null && !hasSignaledReady)
150-
{
151-
// Handle case where InitializeFromState hasn't been called yet
152-
InitializeFromState();
153-
}
154-
else if (currentSchedule != null && hasSignaledReady)
155-
{
156-
// Update individual properties when they change (after initialization)
157-
if (isEnabled != currentSchedule.IsEnabled)
144+
// If we don't have a scheduleId yet (initial state), initialize when CurrentSchedule is set
145+
// But only if we haven't already signaled ready (prevents infinite loop for new schedules with Id=0)
146+
if (scheduleId == 0 && currentSchedule != null && !hasSignaledReady)
158147
{
159-
isEnabled = currentSchedule.IsEnabled;
160-
OnPropertyChanged(nameof(IsEnabled));
148+
InitializeFromState();
149+
return;
161150
}
162-
if (time != new TimeSpan(currentSchedule.Hour, currentSchedule.Minute, currentSchedule.Second))
151+
152+
// Reset hasSignaledReady when schedule ID changes to a different positive ID (existing schedule opened)
153+
if (currentSchedule != null && currentSchedule.Id != scheduleId && currentSchedule.Id > 0)
163154
{
164-
time = new TimeSpan(currentSchedule.Hour, currentSchedule.Minute, currentSchedule.Second);
165-
OnPropertyChanged(nameof(Time));
155+
hasSignaledReady = false;
156+
isReadyActionQueued = false; // Reset queued flag as well
157+
InitializeFromState();
158+
return;
166159
}
167-
if (daysOfWeek != currentSchedule.DaysOfWeek)
160+
161+
// Only update properties if they changed (don't re-initialize)
162+
if (currentSchedule != null && !hasSignaledReady)
168163
{
169-
daysOfWeek = currentSchedule.DaysOfWeek;
170-
OnPropertyChanged(nameof(DaysOfWeek));
164+
// Handle case where InitializeFromState hasn't been called yet
165+
InitializeFromState();
171166
}
172-
if (name != currentSchedule.Name)
167+
else if (currentSchedule != null && hasSignaledReady)
173168
{
174-
name = currentSchedule.Name;
175-
OnPropertyChanged(nameof(Name));
169+
// Update individual properties when they change (after initialization)
170+
if (isEnabled != currentSchedule.IsEnabled)
171+
{
172+
isEnabled = currentSchedule.IsEnabled;
173+
OnPropertyChanged(nameof(IsEnabled));
174+
}
175+
if (time != new TimeSpan(currentSchedule.Hour, currentSchedule.Minute, currentSchedule.Second))
176+
{
177+
time = new TimeSpan(currentSchedule.Hour, currentSchedule.Minute, currentSchedule.Second);
178+
OnPropertyChanged(nameof(Time));
179+
}
180+
if (daysOfWeek != currentSchedule.DaysOfWeek)
181+
{
182+
daysOfWeek = currentSchedule.DaysOfWeek;
183+
OnPropertyChanged(nameof(DaysOfWeek));
184+
}
185+
if (name != currentSchedule.Name)
186+
{
187+
name = currentSchedule.Name;
188+
OnPropertyChanged(nameof(Name));
189+
}
176190
}
177191
}
192+
finally
193+
{
194+
isProcessingStateChange = false;
195+
}
178196
}
179197

180198
public ICommand ToggleDayCommand { get; private set; } = null!;
@@ -326,9 +344,20 @@ private void NotifyScheduleDetailsPropertiesChanged()
326344
OnPropertyChanged(nameof(IsEnabled));
327345
}
328346

347+
private void OnThemeChanged()
348+
{
349+
// Notify DaysOfWeek and IsEnabled properties to trigger converters that bind to them
350+
// This causes day button colors to update when theme changes
351+
// Note: We're already on the main thread (message is sent from main thread),
352+
// but we batch the notifications to ensure MultiBinding converters see both values updated together
353+
OnPropertyChanged(nameof(DaysOfWeek));
354+
OnPropertyChanged(nameof(IsEnabled));
355+
}
356+
329357
public void Dispose()
330358
{
331359
state.StateChanged -= OnStateChanged;
360+
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
332361
}
333362
}
334363

src/Bible.Alarm/ViewModels/ScheduleListItemViewModel.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,9 +551,18 @@ private void OnPlaybackStateChanged(object? sender, EventArgs e)
551551
// The overlay is hidden by AlarmModalService -> ScheduleItemStateService after modal is shown.
552552
}
553553

554+
private void OnThemeChanged()
555+
{
556+
// Notify 'This' property to trigger converters that bind to the entire ViewModel
557+
// This causes day button colors to update when theme changes
558+
// Note: We're already on the main thread (message is sent from main thread)
559+
OnPropertyChanged(nameof(This));
560+
}
561+
554562
public void Dispose()
555563
{
556564
applicationState.StateChanged -= OnApplicationStateChanged;
557565
playbackState.StateChanged -= OnPlaybackStateChanged;
566+
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
558567
}
559568
}

0 commit comments

Comments
 (0)