feat(ui): add ThemedDatePicker control and adopt in Prime settings
New themed picker supports single-date, date+time, and range modes (replaces inconsistent CalendarDatePicker / DatePicker / TimePicker visuals). Used in the Prime schedules row to combine StartDate / EndDate into a single range picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
167
src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml
Normal file
167
src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml
Normal file
@@ -0,0 +1,167 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ClaudeDo.Ui.Views.Controls.ThemedDatePicker"
|
||||
x:Name="Root">
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="ToggleButton.trigger">
|
||||
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="{StaticResource InputCornerRadius}"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="MinHeight" Value="30"/>
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.trigger:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource Surface2Brush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.trigger:checked /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource Surface2Brush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.quick">
|
||||
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
|
||||
<Setter Property="CornerRadius" Value="999"/>
|
||||
<Setter Property="Padding" Value="10,3"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="MinHeight" Value="22"/>
|
||||
</Style>
|
||||
<Style Selector="Button.quick:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource Surface3Brush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrightBrush}"/>
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.nav">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
|
||||
<Setter Property="Padding" Value="6,2"/>
|
||||
<Setter Property="CornerRadius" Value="6"/>
|
||||
<Setter Property="MinWidth" Value="28"/>
|
||||
</Style>
|
||||
<Style Selector="Button.nav:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource Surface3Brush}"/>
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.day">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="Width" Value="32"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="CornerRadius" Value="999"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
</Style>
|
||||
<Style Selector="Button.day:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource Surface3Brush}"/>
|
||||
</Style>
|
||||
<Style Selector="Button.day.outside">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Button.day.today">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Button.day.selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="TextElement.Foreground" Value="White"/>
|
||||
</Style>
|
||||
<Style Selector="Button.day.selected:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentDimBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.weekday">
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextMuteBrush}"/>
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid>
|
||||
<ToggleButton x:Name="TriggerButton" Classes="trigger">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<PathIcon Grid.Column="0" Width="14" Height="14"
|
||||
Margin="0,0,8,0"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
Data="M19,4H18V2H16V4H8V2H6V4H5A2,2 0 0,0 3,6V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V6A2,2 0 0,0 19,4M19,20H5V10H19V20M19,8H5V6H19V8Z"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding #Root.DisplayText}"
|
||||
Foreground="{Binding #Root.DisplayForeground}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<PathIcon Grid.Column="2" Width="10" Height="10"
|
||||
Margin="8,0,0,0"
|
||||
Foreground="{DynamicResource TextMuteBrush}"
|
||||
Data="M7,10L12,15L17,10H7Z"/>
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
|
||||
<Popup x:Name="PickerPopup"
|
||||
PlacementTarget="{Binding #TriggerButton}"
|
||||
Placement="Bottom"
|
||||
IsOpen="{Binding #TriggerButton.IsChecked, Mode=TwoWay}"
|
||||
IsLightDismissEnabled="True">
|
||||
<Border Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10"
|
||||
Padding="14"
|
||||
Margin="0,4,0,0"
|
||||
BoxShadow="{StaticResource ModalShadow}"
|
||||
MinWidth="300">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="quick" Content="Today" Click="OnTodayClick"/>
|
||||
<Button Classes="quick" Content="Tomorrow" Click="OnTomorrowClick"/>
|
||||
<Button Classes="quick" Content="Next Mon" Click="OnNextMondayClick"/>
|
||||
<Button Classes="quick" Content="Clear" Click="OnClearClick"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,2,0,0">
|
||||
<Button Grid.Column="0" Click="OnPrevMonthClick" Classes="nav" Content="◀"/>
|
||||
<TextBlock Grid.Column="1" x:Name="MonthHeader"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
<Button Grid.Column="2" Click="OnNextMonthClick" Classes="nav" Content="▶"/>
|
||||
</Grid>
|
||||
|
||||
<UniformGrid Columns="7" x:Name="WeekdayHeaders"/>
|
||||
|
||||
<UniformGrid Columns="7" Rows="6" x:Name="DayGrid"/>
|
||||
|
||||
<Grid x:Name="TimeRow"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
Margin="0,4,0,0">
|
||||
<TextBlock Grid.Column="0" Text="Time"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBox Grid.Column="1" x:Name="TimeInput"
|
||||
Watermark="HH:mm" MaxLength="5"
|
||||
Text="{Binding #Root.TimeText, Mode=TwoWay}"/>
|
||||
<Button Grid.Column="2" Content="Done"
|
||||
Click="OnDoneClick"
|
||||
Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
423
src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml.cs
Normal file
423
src/ClaudeDo.Ui/Views/Controls/ThemedDatePicker.axaml.cs
Normal file
@@ -0,0 +1,423 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Controls;
|
||||
|
||||
public partial class ThemedDatePicker : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, DateTime?>(
|
||||
nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<bool> ShowTimeProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, bool>(nameof(ShowTime), false);
|
||||
|
||||
public static readonly StyledProperty<bool> IsRangeProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, bool>(nameof(IsRange), false);
|
||||
|
||||
public static readonly StyledProperty<DateTime?> StartDateProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, DateTime?>(
|
||||
nameof(StartDate), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<DateTime?> EndDateProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, DateTime?>(
|
||||
nameof(EndDate), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public static readonly StyledProperty<string?> WatermarkProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, string?>(nameof(Watermark), "Pick a date");
|
||||
|
||||
public static readonly StyledProperty<string?> DisplayTextProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, string?>(nameof(DisplayText));
|
||||
|
||||
public static readonly StyledProperty<IBrush?> DisplayForegroundProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, IBrush?>(nameof(DisplayForeground));
|
||||
|
||||
public static readonly StyledProperty<string?> TimeTextProperty =
|
||||
AvaloniaProperty.Register<ThemedDatePicker, string?>(
|
||||
nameof(TimeText), "09:00",
|
||||
defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public DateTime? SelectedDate
|
||||
{
|
||||
get => GetValue(SelectedDateProperty);
|
||||
set => SetValue(SelectedDateProperty, value);
|
||||
}
|
||||
|
||||
public bool ShowTime
|
||||
{
|
||||
get => GetValue(ShowTimeProperty);
|
||||
set => SetValue(ShowTimeProperty, value);
|
||||
}
|
||||
|
||||
public bool IsRange
|
||||
{
|
||||
get => GetValue(IsRangeProperty);
|
||||
set => SetValue(IsRangeProperty, value);
|
||||
}
|
||||
|
||||
public DateTime? StartDate
|
||||
{
|
||||
get => GetValue(StartDateProperty);
|
||||
set => SetValue(StartDateProperty, value);
|
||||
}
|
||||
|
||||
public DateTime? EndDate
|
||||
{
|
||||
get => GetValue(EndDateProperty);
|
||||
set => SetValue(EndDateProperty, value);
|
||||
}
|
||||
|
||||
public string? Watermark
|
||||
{
|
||||
get => GetValue(WatermarkProperty);
|
||||
set => SetValue(WatermarkProperty, value);
|
||||
}
|
||||
|
||||
public string? DisplayText
|
||||
{
|
||||
get => GetValue(DisplayTextProperty);
|
||||
set => SetValue(DisplayTextProperty, value);
|
||||
}
|
||||
|
||||
public IBrush? DisplayForeground
|
||||
{
|
||||
get => GetValue(DisplayForegroundProperty);
|
||||
set => SetValue(DisplayForegroundProperty, value);
|
||||
}
|
||||
|
||||
public string? TimeText
|
||||
{
|
||||
get => GetValue(TimeTextProperty);
|
||||
set => SetValue(TimeTextProperty, value);
|
||||
}
|
||||
|
||||
private static readonly string[] TimeFormats = { @"h\:mm", @"hh\:mm" };
|
||||
|
||||
private DateTime _displayMonth;
|
||||
private bool _suppressTimeSync;
|
||||
|
||||
public ThemedDatePicker()
|
||||
{
|
||||
InitializeComponent();
|
||||
_displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
|
||||
BuildWeekdayHeaders();
|
||||
BuildDayGrid();
|
||||
UpdateDisplayText();
|
||||
UpdateTimeRowVisibility();
|
||||
|
||||
PickerPopup.Opened += OnPopupOpened;
|
||||
}
|
||||
|
||||
private void UpdateTimeRowVisibility()
|
||||
{
|
||||
if (TimeRow is null) return;
|
||||
TimeRow.IsVisible = ShowTime && !IsRange;
|
||||
}
|
||||
|
||||
private void OnPopupOpened(object? sender, EventArgs e)
|
||||
{
|
||||
var seed = AnchorDate() ?? DateTime.Today;
|
||||
_displayMonth = new DateTime(seed.Year, seed.Month, 1);
|
||||
BuildDayGrid();
|
||||
}
|
||||
|
||||
private DateTime? AnchorDate() =>
|
||||
IsRange ? (StartDate ?? EndDate) : SelectedDate;
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
if (change.Property == SelectedDateProperty)
|
||||
{
|
||||
UpdateDisplayText();
|
||||
SyncTimeFromSelected();
|
||||
BuildDayGrid();
|
||||
}
|
||||
else if (change.Property == StartDateProperty || change.Property == EndDateProperty)
|
||||
{
|
||||
UpdateDisplayText();
|
||||
BuildDayGrid();
|
||||
}
|
||||
else if (change.Property == ShowTimeProperty || change.Property == WatermarkProperty
|
||||
|| change.Property == IsRangeProperty)
|
||||
{
|
||||
UpdateDisplayText();
|
||||
BuildDayGrid();
|
||||
UpdateTimeRowVisibility();
|
||||
}
|
||||
else if (change.Property == TimeTextProperty)
|
||||
{
|
||||
ApplyTimeTextToSelected();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDisplayText()
|
||||
{
|
||||
if (IsRange)
|
||||
{
|
||||
var (s, end) = NormalizeRange(StartDate?.Date, EndDate?.Date);
|
||||
if (s is null && end is null)
|
||||
{
|
||||
DisplayText = Watermark ?? "Pick a range";
|
||||
DisplayForeground = TryGetBrush("TextDimBrush");
|
||||
return;
|
||||
}
|
||||
if (s is not null && end is null)
|
||||
{
|
||||
DisplayText = $"{s.Value:MMM d} – select end";
|
||||
DisplayForeground = TryGetBrush("TextBrush");
|
||||
return;
|
||||
}
|
||||
// both set
|
||||
var sd = s!.Value;
|
||||
var ed = end!.Value;
|
||||
DisplayText = sd.Year == ed.Year
|
||||
? $"{sd:MMM d} – {ed:MMM d, yyyy}"
|
||||
: $"{sd:MMM d, yyyy} – {ed:MMM d, yyyy}";
|
||||
DisplayForeground = TryGetBrush("TextBrush");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedDate is null)
|
||||
{
|
||||
DisplayText = Watermark ?? "Pick a date";
|
||||
DisplayForeground = TryGetBrush("TextDimBrush");
|
||||
return;
|
||||
}
|
||||
var d = SelectedDate.Value;
|
||||
DisplayText = ShowTime
|
||||
? d.ToString("MMM d, yyyy · HH:mm", CultureInfo.CurrentCulture)
|
||||
: d.ToString("MMM d, yyyy", CultureInfo.CurrentCulture);
|
||||
DisplayForeground = TryGetBrush("TextBrush");
|
||||
}
|
||||
|
||||
private static (DateTime? Start, DateTime? End) NormalizeRange(DateTime? a, DateTime? b)
|
||||
{
|
||||
if (a is null && b is null) return (null, null);
|
||||
if (a is null) return (b, b);
|
||||
if (b is null) return (a, null);
|
||||
return a.Value <= b.Value ? (a, b) : (b, a);
|
||||
}
|
||||
|
||||
private IBrush? TryGetBrush(string key)
|
||||
{
|
||||
if (this.TryFindResource(key, out var v) && v is IBrush b) return b;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SyncTimeFromSelected()
|
||||
{
|
||||
if (SelectedDate is null) return;
|
||||
_suppressTimeSync = true;
|
||||
TimeText = SelectedDate.Value.ToString("HH:mm");
|
||||
_suppressTimeSync = false;
|
||||
}
|
||||
|
||||
private void ApplyTimeTextToSelected()
|
||||
{
|
||||
if (_suppressTimeSync || !ShowTime || IsRange || SelectedDate is null) return;
|
||||
if (TryParseTime(TimeText, out var t))
|
||||
{
|
||||
var d = SelectedDate.Value.Date + t;
|
||||
if (d != SelectedDate) SelectedDate = d;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseTime(string? text, out TimeSpan ts)
|
||||
{
|
||||
ts = default;
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
return TimeSpan.TryParseExact(text, TimeFormats, CultureInfo.InvariantCulture, out ts);
|
||||
}
|
||||
|
||||
private void BuildWeekdayHeaders()
|
||||
{
|
||||
if (WeekdayHeaders is null) return;
|
||||
WeekdayHeaders.Children.Clear();
|
||||
var firstDow = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
|
||||
var names = CultureInfo.CurrentCulture.DateTimeFormat.AbbreviatedDayNames;
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var dow = (DayOfWeek)(((int)firstDow + i) % 7);
|
||||
var name = names[(int)dow];
|
||||
if (name.Length > 3) name = name.Substring(0, 3);
|
||||
WeekdayHeaders.Children.Add(new TextBlock { Text = name, Classes = { "weekday" } });
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildDayGrid()
|
||||
{
|
||||
if (DayGrid is null || MonthHeader is null) return;
|
||||
DayGrid.Children.Clear();
|
||||
MonthHeader.Text = _displayMonth.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
|
||||
|
||||
var firstDow = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;
|
||||
var offset = ((int)_displayMonth.DayOfWeek - (int)firstDow + 7) % 7;
|
||||
var start = _displayMonth.AddDays(-offset);
|
||||
|
||||
var today = DateTime.Today;
|
||||
var sel = SelectedDate?.Date;
|
||||
var (rs, re) = NormalizeRange(StartDate?.Date, EndDate?.Date);
|
||||
var rangeFill = TryGetBrush("AccentSoftBrush");
|
||||
|
||||
for (int i = 0; i < 42; i++)
|
||||
{
|
||||
var day = start.AddDays(i);
|
||||
var cell = new Grid();
|
||||
|
||||
if (IsRange && rs.HasValue && re.HasValue && rs.Value != re.Value
|
||||
&& day >= rs.Value && day <= re.Value)
|
||||
{
|
||||
Thickness margin;
|
||||
if (day == rs.Value)
|
||||
margin = new Thickness(19, 0, 0, 0);
|
||||
else if (day == re.Value)
|
||||
margin = new Thickness(0, 0, 19, 0);
|
||||
else
|
||||
margin = default;
|
||||
|
||||
cell.Children.Add(new Border
|
||||
{
|
||||
Background = rangeFill,
|
||||
Margin = margin,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
VerticalAlignment = VerticalAlignment.Stretch
|
||||
});
|
||||
}
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = day.Day.ToString(CultureInfo.CurrentCulture),
|
||||
Classes = { "day" },
|
||||
Tag = day
|
||||
};
|
||||
if (day.Month != _displayMonth.Month) btn.Classes.Add("outside");
|
||||
if (day == today) btn.Classes.Add("today");
|
||||
|
||||
var isSelected = IsRange
|
||||
? (rs.HasValue && day == rs.Value) || (re.HasValue && day == re.Value)
|
||||
: sel.HasValue && day == sel.Value;
|
||||
if (isSelected) btn.Classes.Add("selected");
|
||||
|
||||
btn.Click += OnDayClick;
|
||||
cell.Children.Add(btn);
|
||||
DayGrid.Children.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDayClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button { Tag: DateTime day }) return;
|
||||
|
||||
if (IsRange)
|
||||
{
|
||||
// State A: nothing or both set → start a fresh range
|
||||
// State B: only start set → complete (or restart) range
|
||||
if (StartDate is null || EndDate is not null)
|
||||
{
|
||||
StartDate = day.Date;
|
||||
EndDate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var s = StartDate.Value.Date;
|
||||
if (day.Date < s)
|
||||
{
|
||||
StartDate = day.Date;
|
||||
EndDate = s;
|
||||
}
|
||||
else
|
||||
{
|
||||
EndDate = day.Date;
|
||||
}
|
||||
}
|
||||
BuildDayGrid();
|
||||
return;
|
||||
}
|
||||
|
||||
var time = TimeSpan.Zero;
|
||||
if (ShowTime)
|
||||
{
|
||||
if (TryParseTime(TimeText, out var parsed)) time = parsed;
|
||||
else if (SelectedDate is { } cur) time = cur.TimeOfDay;
|
||||
}
|
||||
else if (SelectedDate is { } cur) time = cur.TimeOfDay;
|
||||
|
||||
SelectedDate = day.Date + time;
|
||||
if (!ShowTime) PickerPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
private void OnPrevMonthClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_displayMonth = _displayMonth.AddMonths(-1);
|
||||
BuildDayGrid();
|
||||
}
|
||||
|
||||
private void OnNextMonthClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_displayMonth = _displayMonth.AddMonths(1);
|
||||
BuildDayGrid();
|
||||
}
|
||||
|
||||
private void OnTodayClick(object? sender, RoutedEventArgs e) => SetQuickDate(DateTime.Today);
|
||||
|
||||
private void OnTomorrowClick(object? sender, RoutedEventArgs e) => SetQuickDate(DateTime.Today.AddDays(1));
|
||||
|
||||
private void OnNextMondayClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
int delta = ((int)DayOfWeek.Monday - (int)today.DayOfWeek + 7) % 7;
|
||||
if (delta == 0) delta = 7;
|
||||
SetQuickDate(today.AddDays(delta));
|
||||
}
|
||||
|
||||
private void OnClearClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (IsRange)
|
||||
{
|
||||
StartDate = null;
|
||||
EndDate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedDate = null;
|
||||
}
|
||||
PickerPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
private void SetQuickDate(DateTime date)
|
||||
{
|
||||
_displayMonth = new DateTime(date.Year, date.Month, 1);
|
||||
|
||||
if (IsRange)
|
||||
{
|
||||
StartDate = date.Date;
|
||||
EndDate = date.Date;
|
||||
BuildDayGrid();
|
||||
PickerPopup.IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var time = TimeSpan.Zero;
|
||||
if (ShowTime)
|
||||
{
|
||||
if (!TryParseTime(TimeText, out time))
|
||||
time = SelectedDate?.TimeOfDay ?? new TimeSpan(9, 0, 0);
|
||||
}
|
||||
SelectedDate = date.Date + time;
|
||||
BuildDayGrid();
|
||||
if (!ShowTime) PickerPopup.IsOpen = false;
|
||||
}
|
||||
|
||||
private void OnDoneClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ApplyTimeTextToSelected();
|
||||
PickerPopup.IsOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
|
||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
||||
x:DataType="vm:SettingsModalViewModel"
|
||||
Title="Settings"
|
||||
@@ -225,23 +226,23 @@
|
||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
||||
Background="{DynamicResource DeepBrush}">
|
||||
<Grid ColumnDefinitions="Auto,*,*,Auto,Auto,Auto,Auto" ColumnSpacing="8">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto" ColumnSpacing="8">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
<CalendarDatePicker Grid.Column="1"
|
||||
SelectedDate="{Binding StartDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||
VerticalAlignment="Center"/>
|
||||
<CalendarDatePicker Grid.Column="2"
|
||||
SelectedDate="{Binding EndDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="3" Width="64"
|
||||
<ctl:ThemedDatePicker Grid.Column="1"
|
||||
IsRange="True"
|
||||
StartDate="{Binding StartDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||
EndDate="{Binding EndDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||
Watermark="Pick a range"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="2" Width="64"
|
||||
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
|
||||
VerticalAlignment="Center"/>
|
||||
<CheckBox Grid.Column="4" Content="Mon–Fri"
|
||||
<CheckBox Grid.Column="3" Content="Mon–Fri"
|
||||
IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="5" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="4" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
|
||||
MinWidth="80"/>
|
||||
<Button Grid.Column="6" Content="✕"
|
||||
<Button Grid.Column="5" Content="✕"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user