feat(ui): add MarkdownView control and editable description in details island
New MarkdownView UserControl renders a markdown preview. Details island gains an editable Description section with edit/preview toggle, collapsible header, copy-to-clipboard, and debounced auto-save (400ms). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,9 +26,33 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
|
|
||||||
// Editable fields
|
// Editable fields
|
||||||
[ObservableProperty] private string _editableTitle = "";
|
[ObservableProperty] private string _editableTitle = "";
|
||||||
|
[ObservableProperty] private string _editableDescription = "";
|
||||||
|
[ObservableProperty] private bool _isEditingDescription;
|
||||||
|
[ObservableProperty] private bool _isDescriptionExpanded = true;
|
||||||
[ObservableProperty] private string _notes = "";
|
[ObservableProperty] private string _notes = "";
|
||||||
[ObservableProperty] private string _promptInput = "";
|
[ObservableProperty] private string _promptInput = "";
|
||||||
|
|
||||||
|
public bool IsDescriptionEditorVisible => IsDescriptionExpanded && IsEditingDescription;
|
||||||
|
public bool IsDescriptionPreviewVisible => IsDescriptionExpanded && !IsEditingDescription;
|
||||||
|
|
||||||
|
partial void OnIsDescriptionExpandedChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsDescriptionEditorVisible));
|
||||||
|
OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsEditingDescriptionChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsDescriptionEditorVisible));
|
||||||
|
OnPropertyChanged(nameof(IsDescriptionPreviewVisible));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleEditDescription() => IsEditingDescription = !IsEditingDescription;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleDescriptionExpanded() => IsDescriptionExpanded = !IsDescriptionExpanded;
|
||||||
|
|
||||||
// Short task-id badge, e.g. "#T1A"
|
// Short task-id badge, e.g. "#T1A"
|
||||||
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
||||||
|
|
||||||
@@ -78,6 +102,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
private bool _suppressAgentSave;
|
private bool _suppressAgentSave;
|
||||||
private CancellationTokenSource? _agentSaveCts;
|
private CancellationTokenSource? _agentSaveCts;
|
||||||
|
|
||||||
|
private bool _suppressDescSave;
|
||||||
|
private CancellationTokenSource? _descSaveCts;
|
||||||
|
|
||||||
public bool IsAgentSectionEnabled => !IsRunning;
|
public bool IsAgentSectionEnabled => !IsRunning;
|
||||||
|
|
||||||
[ObservableProperty] private string? _worktreePath;
|
[ObservableProperty] private string? _worktreePath;
|
||||||
@@ -269,6 +296,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
|
||||||
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();
|
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();
|
||||||
|
|
||||||
|
partial void OnEditableDescriptionChanged(string value)
|
||||||
|
{
|
||||||
|
if (_suppressDescSave || Task is null) return;
|
||||||
|
_descSaveCts?.Cancel();
|
||||||
|
_descSaveCts = new CancellationTokenSource();
|
||||||
|
_ = SaveDescriptionAsync(_descSaveCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async System.Threading.Tasks.Task SaveDescriptionAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await System.Threading.Tasks.Task.Delay(400, ct);
|
||||||
|
if (Task is null) return;
|
||||||
|
await using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var repo = new TaskRepository(ctx);
|
||||||
|
var entity = await repo.GetByIdAsync(Task.Id);
|
||||||
|
if (entity is null) return;
|
||||||
|
entity.Description = string.IsNullOrWhiteSpace(EditableDescription) ? null : EditableDescription;
|
||||||
|
await repo.UpdateAsync(entity);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
private void QueueAgentSave()
|
private void QueueAgentSave()
|
||||||
{
|
{
|
||||||
if (_suppressAgentSave || Task is null) return;
|
if (_suppressAgentSave || Task is null) return;
|
||||||
@@ -348,6 +400,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_subscribedTaskId = null;
|
_subscribedTaskId = null;
|
||||||
EditableTitle = "";
|
EditableTitle = "";
|
||||||
|
EditableDescription = "";
|
||||||
Notes = "";
|
Notes = "";
|
||||||
Model = null;
|
Model = null;
|
||||||
WorktreePath = null;
|
WorktreePath = null;
|
||||||
@@ -392,6 +445,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
if (entity == null) return;
|
if (entity == null) return;
|
||||||
|
|
||||||
EditableTitle = entity.Title;
|
EditableTitle = entity.Title;
|
||||||
|
_suppressDescSave = true;
|
||||||
|
try { EditableDescription = entity.Description ?? ""; }
|
||||||
|
finally { _suppressDescSave = false; }
|
||||||
Notes = entity.Notes ?? "";
|
Notes = entity.Notes ?? "";
|
||||||
Model = entity.Model;
|
Model = entity.Model;
|
||||||
WorktreePath = entity.Worktree?.Path;
|
WorktreePath = entity.Worktree?.Path;
|
||||||
|
|||||||
5
src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml
Normal file
5
src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Controls.MarkdownView">
|
||||||
|
<StackPanel x:Name="Host" Spacing="6"/>
|
||||||
|
</UserControl>
|
||||||
231
src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml.cs
Normal file
231
src/ClaudeDo.Ui/Views/Controls/MarkdownView.axaml.cs
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Documents;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Controls;
|
||||||
|
|
||||||
|
public partial class MarkdownView : UserControl
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<string?> MarkdownProperty =
|
||||||
|
AvaloniaProperty.Register<MarkdownView, string?>(nameof(Markdown));
|
||||||
|
|
||||||
|
public string? Markdown
|
||||||
|
{
|
||||||
|
get => GetValue(MarkdownProperty);
|
||||||
|
set => SetValue(MarkdownProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StackPanel? _host;
|
||||||
|
|
||||||
|
public MarkdownView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_host = this.FindControl<StackPanel>("Host");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
if (change.Property == MarkdownProperty)
|
||||||
|
Render(change.GetNewValue<string?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Regex NumberedItem = new(@"^\s*(\d+)\.\s+(.*)$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private void Render(string? md)
|
||||||
|
{
|
||||||
|
_host ??= this.FindControl<StackPanel>("Host");
|
||||||
|
if (_host is null) return;
|
||||||
|
_host.Children.Clear();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(md))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var lines = md.Replace("\r\n", "\n").Split('\n');
|
||||||
|
int i = 0;
|
||||||
|
while (i < lines.Length)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) { i++; continue; }
|
||||||
|
|
||||||
|
if (line.StartsWith("### "))
|
||||||
|
{
|
||||||
|
_host.Children.Add(Heading(line[4..], 13));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("## "))
|
||||||
|
{
|
||||||
|
_host.Children.Add(Heading(line[3..], 15));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.StartsWith("# "))
|
||||||
|
{
|
||||||
|
_host.Children.Add(Heading(line[2..], 17));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("- ") || line.StartsWith("* "))
|
||||||
|
{
|
||||||
|
while (i < lines.Length && (lines[i].StartsWith("- ") || lines[i].StartsWith("* ")))
|
||||||
|
{
|
||||||
|
_host.Children.Add(Bullet(lines[i][2..]));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var num = NumberedItem.Match(line);
|
||||||
|
if (num.Success)
|
||||||
|
{
|
||||||
|
while (i < lines.Length && NumberedItem.IsMatch(lines[i]))
|
||||||
|
{
|
||||||
|
var m = NumberedItem.Match(lines[i]);
|
||||||
|
_host.Children.Add(Numbered(m.Groups[1].Value, m.Groups[2].Value));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paragraph = new System.Text.StringBuilder();
|
||||||
|
while (i < lines.Length
|
||||||
|
&& !string.IsNullOrWhiteSpace(lines[i])
|
||||||
|
&& !IsBlockStart(lines[i]))
|
||||||
|
{
|
||||||
|
if (paragraph.Length > 0) paragraph.Append(' ');
|
||||||
|
paragraph.Append(lines[i].Trim());
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
_host.Children.Add(Paragraph(paragraph.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBlockStart(string line) =>
|
||||||
|
line.StartsWith("# ") || line.StartsWith("## ") || line.StartsWith("### ")
|
||||||
|
|| line.StartsWith("- ") || line.StartsWith("* ")
|
||||||
|
|| NumberedItem.IsMatch(line);
|
||||||
|
|
||||||
|
private static Control Heading(string text, double size)
|
||||||
|
{
|
||||||
|
var tb = new SelectableTextBlock
|
||||||
|
{
|
||||||
|
FontSize = size,
|
||||||
|
FontWeight = FontWeight.SemiBold,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Margin = new Thickness(0, 4, 0, 2),
|
||||||
|
};
|
||||||
|
AppendInlines(tb.Inlines!, text);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Control Paragraph(string text)
|
||||||
|
{
|
||||||
|
var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
|
||||||
|
AppendInlines(tb.Inlines!, text);
|
||||||
|
return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Control Bullet(string text)
|
||||||
|
{
|
||||||
|
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("14,*") };
|
||||||
|
var dot = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "•",
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 0, 4, 0),
|
||||||
|
};
|
||||||
|
Grid.SetColumn(dot, 0);
|
||||||
|
grid.Children.Add(dot);
|
||||||
|
|
||||||
|
var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
|
||||||
|
AppendInlines(tb.Inlines!, text);
|
||||||
|
Grid.SetColumn(tb, 1);
|
||||||
|
grid.Children.Add(tb);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Control Numbered(string num, string text)
|
||||||
|
{
|
||||||
|
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("20,*") };
|
||||||
|
var lbl = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{num}.",
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 0, 4, 0),
|
||||||
|
};
|
||||||
|
Grid.SetColumn(lbl, 0);
|
||||||
|
grid.Children.Add(lbl);
|
||||||
|
|
||||||
|
var tb = new SelectableTextBlock { TextWrapping = TextWrapping.Wrap };
|
||||||
|
AppendInlines(tb.Inlines!, text);
|
||||||
|
Grid.SetColumn(tb, 1);
|
||||||
|
grid.Children.Add(tb);
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendInlines(InlineCollection inlines, string text)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
var plain = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
void FlushPlain()
|
||||||
|
{
|
||||||
|
if (plain.Length == 0) return;
|
||||||
|
inlines.Add(new Run(plain.ToString()));
|
||||||
|
plain.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < text.Length)
|
||||||
|
{
|
||||||
|
if (i + 1 < text.Length && text[i] == '*' && text[i + 1] == '*')
|
||||||
|
{
|
||||||
|
int close = text.IndexOf("**", i + 2, System.StringComparison.Ordinal);
|
||||||
|
if (close > i + 2)
|
||||||
|
{
|
||||||
|
FlushPlain();
|
||||||
|
inlines.Add(new Run(text[(i + 2)..close]) { FontWeight = FontWeight.Bold });
|
||||||
|
i = close + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text[i] == '*')
|
||||||
|
{
|
||||||
|
int close = text.IndexOf('*', i + 1);
|
||||||
|
if (close > i + 1)
|
||||||
|
{
|
||||||
|
FlushPlain();
|
||||||
|
inlines.Add(new Run(text[(i + 1)..close]) { FontStyle = FontStyle.Italic });
|
||||||
|
i = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text[i] == '`')
|
||||||
|
{
|
||||||
|
int close = text.IndexOf('`', i + 1);
|
||||||
|
if (close > i + 1)
|
||||||
|
{
|
||||||
|
FlushPlain();
|
||||||
|
inlines.Add(new Run(text[(i + 1)..close])
|
||||||
|
{
|
||||||
|
FontFamily = new FontFamily("Consolas,Menlo,monospace"),
|
||||||
|
Background = new SolidColorBrush(Color.FromArgb(60, 127, 127, 127)),
|
||||||
|
});
|
||||||
|
i = close + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plain.Append(text[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
FlushPlain();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||||
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
|
||||||
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
|
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
|
||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
@@ -214,6 +215,77 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Details (description) section -->
|
||||||
|
<Border Padding="18,12,18,12"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
Classes="flat"
|
||||||
|
Command="{Binding ToggleDescriptionExpandedCommand}"
|
||||||
|
Padding="0"
|
||||||
|
Margin="0,0,6,2"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock Text="▾" FontSize="10"
|
||||||
|
IsVisible="{Binding IsDescriptionExpanded}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
<TextBlock Text="▸" FontSize="10"
|
||||||
|
IsVisible="{Binding !IsDescriptionExpanded}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
<TextBlock Classes="section-label" Text="DETAILS"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Padding="6,2"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
ToolTip.Tip="Copy description to clipboard"
|
||||||
|
IsVisible="{Binding IsDescriptionExpanded}"
|
||||||
|
Click="OnCopyDescriptionClick">
|
||||||
|
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Command="{Binding ToggleEditDescriptionCommand}"
|
||||||
|
Padding="6,2"
|
||||||
|
FontSize="10"
|
||||||
|
ToolTip.Tip="Toggle edit/preview"
|
||||||
|
IsVisible="{Binding IsDescriptionEditorVisible}">
|
||||||
|
<TextBlock Text="Preview"/>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Command="{Binding ToggleEditDescriptionCommand}"
|
||||||
|
Padding="6,2"
|
||||||
|
FontSize="10"
|
||||||
|
ToolTip.Tip="Toggle edit/preview"
|
||||||
|
IsVisible="{Binding IsDescriptionPreviewVisible}">
|
||||||
|
<TextBlock Text="Edit"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBox Text="{Binding EditableDescription, Mode=TwoWay}"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
MinHeight="80"
|
||||||
|
MaxHeight="320"
|
||||||
|
PlaceholderText="Add task details (markdown supported)..."
|
||||||
|
Padding="8"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="12"
|
||||||
|
Background="{DynamicResource Surface2Brush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="6"
|
||||||
|
IsVisible="{Binding IsDescriptionEditorVisible}"/>
|
||||||
|
|
||||||
|
<ctl:MarkdownView Markdown="{Binding EditableDescription}"
|
||||||
|
IsVisible="{Binding IsDescriptionPreviewVisible}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Session terminal — auto-sizes to output, scrolls internally after MaxHeight -->
|
<!-- Session terminal — auto-sizes to output, scrolls internally after MaxHeight -->
|
||||||
<islands:SessionTerminalView MaxHeight="420"/>
|
<islands:SessionTerminalView MaxHeight="420"/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
@@ -147,4 +148,12 @@ public partial class DetailsIslandView : UserControl
|
|||||||
if (DataContext is DetailsIslandViewModel vm)
|
if (DataContext is DetailsIslandViewModel vm)
|
||||||
vm.SaveNotesCommand.Execute(null);
|
vm.SaveNotesCommand.Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnCopyDescriptionClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not DetailsIslandViewModel vm) return;
|
||||||
|
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
|
||||||
|
if (clipboard is null) return;
|
||||||
|
await clipboard.SetTextAsync(vm.EditableDescription ?? string.Empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user