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:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
|
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
|
||||||
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
xmlns:conv="using:ClaudeDo.Ui.Converters"
|
||||||
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||||
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
||||||
x:DataType="vm:SettingsModalViewModel"
|
x:DataType="vm:SettingsModalViewModel"
|
||||||
Title="Settings"
|
Title="Settings"
|
||||||
@@ -225,23 +226,23 @@
|
|||||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
||||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
||||||
Background="{DynamicResource DeepBrush}">
|
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"/>
|
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||||
<CalendarDatePicker Grid.Column="1"
|
<ctl:ThemedDatePicker Grid.Column="1"
|
||||||
SelectedDate="{Binding StartDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
IsRange="True"
|
||||||
|
StartDate="{Binding StartDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||||
|
EndDate="{Binding EndDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
||||||
|
Watermark="Pick a range"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<CalendarDatePicker Grid.Column="2"
|
<TextBox Grid.Column="2" Width="64"
|
||||||
SelectedDate="{Binding EndDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Column="3" Width="64"
|
|
||||||
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
|
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<CheckBox Grid.Column="4" Content="Mon–Fri"
|
<CheckBox Grid.Column="3" Content="Mon–Fri"
|
||||||
IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/>
|
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"
|
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
|
||||||
MinWidth="80"/>
|
MinWidth="80"/>
|
||||||
<Button Grid.Column="6" Content="✕"
|
<Button Grid.Column="5" Content="✕"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
Reference in New Issue
Block a user