feat: add State B review/edit panel to NewListingView
This commit is contained in:
@@ -106,8 +106,304 @@
|
|||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════ STATE B: Review & Edit (stub for now) -->
|
<!-- ══════════════════════════════════════ STATE B: Review & Edit -->
|
||||||
<Grid x:Name="StateB" Visibility="Collapsed"/>
|
<Grid x:Name="StateB" Visibility="Collapsed">
|
||||||
|
<DockPanel LastChildFill="True">
|
||||||
|
|
||||||
|
<!-- Footer bar — pinned to bottom via DockPanel.Dock -->
|
||||||
|
<Border DockPanel.Dock="Bottom"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray9}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
BorderBrush="{DynamicResource MahApps.Brushes.Gray7}"
|
||||||
|
Padding="16,8">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Button Grid.Column="0" x:Name="StartOverBtn"
|
||||||
|
Click="StartOver_Click"
|
||||||
|
Background="Transparent" BorderThickness="0"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Cursor="Hand" VerticalAlignment="Center">
|
||||||
|
<TextBlock FontSize="11">
|
||||||
|
<Run Text="← "/>
|
||||||
|
<Run Text="Start Over" TextDecorations="Underline"/>
|
||||||
|
</TextBlock>
|
||||||
|
</Button>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||||
|
<Button x:Name="SaveDraftBtn"
|
||||||
|
Click="SaveDraft_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square}"
|
||||||
|
Padding="16,8" Margin="0,0,8,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial Kind="ContentSaveOutline"
|
||||||
|
Width="14" Height="14" Margin="0,0,6,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Save as Draft" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="PostBtn"
|
||||||
|
Click="Post_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square.Accent}"
|
||||||
|
Padding="16,8">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="PostIcon"
|
||||||
|
Kind="CartArrowRight" Width="14" Height="14"
|
||||||
|
Margin="0,0,6,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="PostSpinner"
|
||||||
|
Width="14" Height="14" Margin="0,0,6,0"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
<TextBlock Text="Post to eBay" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Two-column content area -->
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="16,12,16,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="220"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- LEFT: Photos panel -->
|
||||||
|
<DockPanel Grid.Column="0">
|
||||||
|
<TextBlock DockPanel.Dock="Top"
|
||||||
|
Text="PHOTOS" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,8,0,0">
|
||||||
|
<Button x:Name="AddMorePhotosBtn" Click="AddMorePhotos_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square}"
|
||||||
|
Padding="8,4" FontSize="11">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Plus" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Add more" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<TextBlock x:Name="BPhotoCount"
|
||||||
|
Margin="8,0,0,0" VerticalAlignment="Center"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<WrapPanel x:Name="BPhotosPanel"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- RIGHT: Listing fields -->
|
||||||
|
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="0,0,8,16">
|
||||||
|
<TextBlock Text="LISTING DETAILS"
|
||||||
|
FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
|
||||||
|
Margin="0,0,0,12"/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<Grid Margin="0,0,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Title" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" x:Name="AiTitleBtn" Click="AiTitle_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square}"
|
||||||
|
Padding="6,2" ToolTip="Improve title with AI">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="TitleAiIcon"
|
||||||
|
Kind="AutoFix" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="TitleSpinner" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" Visibility="Collapsed"/>
|
||||||
|
<TextBlock Text="AI" FontSize="10" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="BTitleBox" TextChanged="TitleBox_TextChanged"
|
||||||
|
MaxLength="80" Margin="0,0,0,2"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Height="3" CornerRadius="1.5"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray8}">
|
||||||
|
<Border x:Name="BTitleBar" Height="3" CornerRadius="1.5"
|
||||||
|
HorizontalAlignment="Left" Width="0"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Accent}"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="BTitleCount" Grid.Column="1"
|
||||||
|
Text="0 / 80" FontSize="10" Margin="6,0,0,0"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<Grid Margin="0,0,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Description" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" x:Name="AiDescBtn" Click="AiDesc_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square}"
|
||||||
|
Padding="6,2" ToolTip="Write description with AI">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="DescAiIcon"
|
||||||
|
Kind="AutoFix" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="DescSpinner" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" Visibility="Collapsed"/>
|
||||||
|
<TextBlock Text="AI" FontSize="10" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<TextBox x:Name="BDescBox" TextChanged="DescBox_TextChanged"
|
||||||
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
Height="110" VerticalScrollBarVisibility="Auto"
|
||||||
|
Margin="0,0,0,2"/>
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Height="3" CornerRadius="1.5"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray8}">
|
||||||
|
<Border x:Name="BDescBar" Height="3" CornerRadius="1.5"
|
||||||
|
HorizontalAlignment="Left" Width="0"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Accent}"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="BDescCount" Grid.Column="1"
|
||||||
|
Text="0 / 2000" FontSize="10" Margin="6,0,0,0"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Category -->
|
||||||
|
<TextBlock Text="Category" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<Grid Margin="0,0,0,2">
|
||||||
|
<TextBox x:Name="BCategoryBox"
|
||||||
|
TextChanged="CategoryBox_TextChanged"
|
||||||
|
KeyDown="CategoryBox_KeyDown"
|
||||||
|
mah:TextBoxHelper.Watermark="Type to search categories…"/>
|
||||||
|
<ListBox x:Name="BCategoryList"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
SelectionChanged="CategoryList_SelectionChanged"
|
||||||
|
MaxHeight="160"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,32,0,0"
|
||||||
|
Panel.ZIndex="10"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray8}"
|
||||||
|
BorderBrush="{DynamicResource MahApps.Brushes.Gray6}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock x:Name="BCategoryIdLabel"
|
||||||
|
Text="(no category selected)"
|
||||||
|
FontSize="10" Margin="0,0,0,12"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
|
||||||
|
<!-- Condition + Format -->
|
||||||
|
<Grid Margin="0,0,0,12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Condition" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="BConditionBox"
|
||||||
|
SelectionChanged="ConditionBox_SelectionChanged">
|
||||||
|
<ComboBoxItem Content="New" Tag="New"/>
|
||||||
|
<ComboBoxItem Content="Open Box" Tag="OpenBox"/>
|
||||||
|
<ComboBoxItem Content="Refurbished" Tag="Refurbished"/>
|
||||||
|
<ComboBoxItem Content="Used" Tag="Used" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="For Parts" Tag="ForParts"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock Text="Format" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="BFormatBox">
|
||||||
|
<ComboBoxItem Content="Fixed Price" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="Auction"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Price -->
|
||||||
|
<Grid Margin="0,0,0,4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Price" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" x:Name="AiPriceBtn" Click="AiPrice_Click"
|
||||||
|
Style="{StaticResource MahApps.Styles.Button.Square}"
|
||||||
|
Padding="6,2" ToolTip="Research live eBay price">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="PriceAiIcon"
|
||||||
|
Kind="Magnify" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" VerticalAlignment="Center"/>
|
||||||
|
<mah:ProgressRing x:Name="PriceSpinner" Width="12" Height="12"
|
||||||
|
Margin="0,0,4,0" Visibility="Collapsed"/>
|
||||||
|
<TextBlock Text="Research" FontSize="10" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<mah:NumericUpDown x:Name="BPriceBox"
|
||||||
|
StringFormat="£{0:0.00}"
|
||||||
|
Minimum="0" Maximum="99999"
|
||||||
|
Interval="0.50"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<TextBlock x:Name="BPriceHint"
|
||||||
|
FontSize="10" Margin="0,0,0,12"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray5}"/>
|
||||||
|
|
||||||
|
<!-- Postage + Postcode -->
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="12"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="Postage" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<ComboBox x:Name="BPostageBox">
|
||||||
|
<ComboBoxItem Content="Royal Mail 1st Class" Tag="RoyalMailFirstClass"/>
|
||||||
|
<ComboBoxItem Content="Royal Mail 2nd Class" Tag="RoyalMailSecondClass" IsSelected="True"/>
|
||||||
|
<ComboBoxItem Content="Royal Mail Tracked 24" Tag="RoyalMailTracked24"/>
|
||||||
|
<ComboBoxItem Content="Royal Mail Tracked 48" Tag="RoyalMailTracked48"/>
|
||||||
|
<ComboBoxItem Content="Collection Only" Tag="CollectionOnly"/>
|
||||||
|
<ComboBoxItem Content="Free Postage" Tag="FreePostage"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2">
|
||||||
|
<TextBlock Text="From postcode" FontSize="11" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Gray3}"
|
||||||
|
Margin="0,0,0,4"/>
|
||||||
|
<TextBox x:Name="BPostcodeBox"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<!-- ══════════════════════════════════════ STATE C: Success (stub for now) -->
|
<!-- ══════════════════════════════════════ STATE C: Success (stub for now) -->
|
||||||
<Grid x:Name="StateC" Visibility="Collapsed"/>
|
<Grid x:Name="StateC" Visibility="Collapsed"/>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public partial class NewListingView : UserControl
|
|||||||
private readonly List<string> _photoPaths = new();
|
private readonly List<string> _photoPaths = new();
|
||||||
private const int MaxPhotos = 12;
|
private const int MaxPhotos = 12;
|
||||||
|
|
||||||
// State B — draft being edited (stub, populated in Task 4)
|
// State B — draft being edited
|
||||||
private ListingDraft _draft = new();
|
private ListingDraft _draft = new();
|
||||||
private PhotoAnalysisResult? _lastAnalysis;
|
private PhotoAnalysisResult? _lastAnalysis;
|
||||||
private bool _suppressCategoryLookup;
|
private bool _suppressCategoryLookup;
|
||||||
@@ -211,10 +211,398 @@ public partial class NewListingView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stub for State B — implemented in Task 4
|
// ---- State B: Populate from analysis ----
|
||||||
private Task PopulateStateBAsync(PhotoAnalysisResult result) => Task.CompletedTask;
|
|
||||||
|
private async Task PopulateStateBAsync(PhotoAnalysisResult result)
|
||||||
|
{
|
||||||
|
_draft = new ListingDraft { Postcode = _defaultPostcode };
|
||||||
|
_draft.PhotoPaths = new List<string>(_photoPaths);
|
||||||
|
RebuildBPhotoThumbnails();
|
||||||
|
|
||||||
|
BTitleBox.Text = result.Title;
|
||||||
|
BDescBox.Text = result.Description;
|
||||||
|
BPriceBox.Value = (double)Math.Round(result.PriceSuggested, 2);
|
||||||
|
BPostcodeBox.Text = _defaultPostcode;
|
||||||
|
BConditionBox.SelectedIndex = 3; // Used
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(result.CategoryKeyword))
|
||||||
|
await AutoFillCategoryAsync(result.CategoryKeyword);
|
||||||
|
|
||||||
|
if (result.PriceMin > 0 && result.PriceMax > 0)
|
||||||
|
{
|
||||||
|
BPriceHint.Text = $"AI estimate: £{result.PriceMin:F2} – £{result.PriceMax:F2}";
|
||||||
|
BPriceHint.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Title ----
|
||||||
|
|
||||||
|
private void TitleBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_draft.Title = BTitleBox.Text;
|
||||||
|
var len = BTitleBox.Text.Length;
|
||||||
|
BTitleCount.Text = $"{len} / 80";
|
||||||
|
var over = len > 75;
|
||||||
|
var trackBorder = BTitleBar.Parent as Border;
|
||||||
|
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
||||||
|
if (trackWidth > 0) BTitleBar.Width = trackWidth * (len / 80.0);
|
||||||
|
BTitleBar.Background = over
|
||||||
|
? System.Windows.Media.Brushes.OrangeRed
|
||||||
|
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Accent");
|
||||||
|
BTitleCount.Foreground = over
|
||||||
|
? System.Windows.Media.Brushes.OrangeRed
|
||||||
|
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AiTitle_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_aiService == null) return;
|
||||||
|
SetTitleBusy(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var title = await _aiService.GenerateTitleAsync(BTitleBox.Text, GetSelectedCondition().ToString());
|
||||||
|
BTitleBox.Text = title.Trim().TrimEnd('.').Trim('"');
|
||||||
|
if (string.IsNullOrWhiteSpace(_draft.CategoryId))
|
||||||
|
await AutoFillCategoryAsync(BTitleBox.Text);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError("AI Title", ex.Message); }
|
||||||
|
finally { SetTitleBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTitleBusy(bool busy)
|
||||||
|
{
|
||||||
|
AiTitleBtn.IsEnabled = !busy;
|
||||||
|
TitleSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
TitleAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Description ----
|
||||||
|
|
||||||
|
private void DescBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_draft.Description = BDescBox.Text;
|
||||||
|
var len = BDescBox.Text.Length;
|
||||||
|
const int softCap = 2000;
|
||||||
|
BDescCount.Text = $"{len} / {softCap}";
|
||||||
|
var over = len > softCap;
|
||||||
|
var trackBorder = BDescBar.Parent as Border;
|
||||||
|
double trackWidth = trackBorder?.ActualWidth ?? 0;
|
||||||
|
if (trackWidth > 0) BDescBar.Width = Math.Min(trackWidth, trackWidth * (len / (double)softCap));
|
||||||
|
BDescBar.Background = over
|
||||||
|
? System.Windows.Media.Brushes.OrangeRed
|
||||||
|
: new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xF5, 0x9E, 0x0B));
|
||||||
|
BDescCount.Foreground = over
|
||||||
|
? System.Windows.Media.Brushes.OrangeRed
|
||||||
|
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AiDesc_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_aiService == null) return;
|
||||||
|
SetDescBusy(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var desc = await _aiService.WriteDescriptionAsync(
|
||||||
|
BTitleBox.Text, GetSelectedCondition().ToString(), BDescBox.Text);
|
||||||
|
BDescBox.Text = desc;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError("AI Description", ex.Message); }
|
||||||
|
finally { SetDescBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDescBusy(bool busy)
|
||||||
|
{
|
||||||
|
AiDescBtn.IsEnabled = !busy;
|
||||||
|
DescSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
DescAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Category ----
|
||||||
|
|
||||||
|
private void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_suppressCategoryLookup) return;
|
||||||
|
_categoryCts?.Cancel();
|
||||||
|
_categoryCts?.Dispose();
|
||||||
|
_categoryCts = new System.Threading.CancellationTokenSource();
|
||||||
|
var cts = _categoryCts;
|
||||||
|
if (BCategoryBox.Text.Length < 3) { BCategoryList.Visibility = Visibility.Collapsed; return; }
|
||||||
|
_ = SearchCategoryAsync(BCategoryBox.Text, cts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SearchCategoryAsync(string text, System.Threading.CancellationTokenSource cts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(350, cts.Token);
|
||||||
|
if (cts.IsCancellationRequested) return;
|
||||||
|
var suggestions = await _categoryService!.GetCategorySuggestionsAsync(text);
|
||||||
|
if (cts.IsCancellationRequested) return;
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
BCategoryList.ItemsSource = suggestions.Select(s => s.CategoryName).ToList();
|
||||||
|
BCategoryList.Tag = suggestions;
|
||||||
|
BCategoryList.Visibility = suggestions.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CategoryBox_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Escape) { BCategoryList.Visibility = Visibility.Collapsed; e.Handled = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CategoryList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (BCategoryList.SelectedIndex < 0) return;
|
||||||
|
var suggestions = BCategoryList.Tag as List<CategorySuggestion>;
|
||||||
|
if (suggestions == null || BCategoryList.SelectedIndex >= suggestions.Count) return;
|
||||||
|
var cat = suggestions[BCategoryList.SelectedIndex];
|
||||||
|
_suppressCategoryLookup = true;
|
||||||
|
_draft.CategoryId = cat.CategoryId;
|
||||||
|
_draft.CategoryName = cat.CategoryName;
|
||||||
|
BCategoryBox.Text = cat.CategoryName;
|
||||||
|
BCategoryIdLabel.Text = $"ID: {cat.CategoryId}";
|
||||||
|
BCategoryList.Visibility = Visibility.Collapsed;
|
||||||
|
_suppressCategoryLookup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AutoFillCategoryAsync(string keyword)
|
||||||
|
{
|
||||||
|
if (_categoryService == null || string.IsNullOrWhiteSpace(keyword)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var suggestions = await _categoryService.GetCategorySuggestionsAsync(keyword);
|
||||||
|
if (suggestions.Count == 0) return;
|
||||||
|
var top = suggestions[0];
|
||||||
|
_suppressCategoryLookup = true;
|
||||||
|
_draft.CategoryId = top.CategoryId;
|
||||||
|
_draft.CategoryName = top.CategoryName;
|
||||||
|
BCategoryBox.Text = top.CategoryName;
|
||||||
|
BCategoryIdLabel.Text = $"ID: {top.CategoryId}";
|
||||||
|
_suppressCategoryLookup = false;
|
||||||
|
BCategoryList.ItemsSource = suggestions.Select(s => s.CategoryName).ToList();
|
||||||
|
BCategoryList.Tag = suggestions;
|
||||||
|
BCategoryList.Visibility = suggestions.Count > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Condition ----
|
||||||
|
|
||||||
|
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_draft.Condition = GetSelectedCondition();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ItemCondition GetSelectedCondition()
|
||||||
|
{
|
||||||
|
var tag = (BConditionBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Used";
|
||||||
|
return tag switch
|
||||||
|
{
|
||||||
|
"New" => ItemCondition.New,
|
||||||
|
"OpenBox" => ItemCondition.OpenBox,
|
||||||
|
"Refurbished" => ItemCondition.Refurbished,
|
||||||
|
"ForParts" => ItemCondition.ForPartsOrNotWorking,
|
||||||
|
_ => ItemCondition.Used
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Price ----
|
||||||
|
|
||||||
|
private async void AiPrice_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_aiService == null) return;
|
||||||
|
SetPriceBusy(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _aiService.SuggestPriceAsync(BTitleBox.Text, GetSelectedCondition().ToString());
|
||||||
|
var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var priceLine = lines.FirstOrDefault(l => l.StartsWith("PRICE:", StringComparison.OrdinalIgnoreCase));
|
||||||
|
_suggestedPriceValue = priceLine?.Replace("PRICE:", "", StringComparison.OrdinalIgnoreCase).Trim() ?? "";
|
||||||
|
BPriceHint.Text = lines.FirstOrDefault() ?? result;
|
||||||
|
BPriceHint.Visibility = Visibility.Visible;
|
||||||
|
if (decimal.TryParse(_suggestedPriceValue, out var price))
|
||||||
|
BPriceBox.Value = (double)price;
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError("AI Price", ex.Message); }
|
||||||
|
finally { SetPriceBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPriceBusy(bool busy)
|
||||||
|
{
|
||||||
|
AiPriceBtn.IsEnabled = !busy;
|
||||||
|
PriceSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
PriceAiIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Photos (State B) ----
|
||||||
|
|
||||||
|
private void RebuildBPhotoThumbnails()
|
||||||
|
{
|
||||||
|
BPhotosPanel.Children.Clear();
|
||||||
|
for (int i = 0; i < _draft.PhotoPaths.Count; i++)
|
||||||
|
AddBPhotoThumbnail(_draft.PhotoPaths[i], i);
|
||||||
|
BPhotoCount.Text = $"{_draft.PhotoPaths.Count} / {MaxPhotos}";
|
||||||
|
BPhotoCount.Foreground = _draft.PhotoPaths.Count >= MaxPhotos
|
||||||
|
? System.Windows.Media.Brushes.OrangeRed
|
||||||
|
: (System.Windows.Media.Brush)FindResource("MahApps.Brushes.Gray5");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBPhotoThumbnail(string path, int index)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bmp = new BitmapImage();
|
||||||
|
bmp.BeginInit();
|
||||||
|
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
||||||
|
bmp.DecodePixelWidth = 128;
|
||||||
|
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
bmp.EndInit();
|
||||||
|
bmp.Freeze();
|
||||||
|
|
||||||
|
var img = new Image
|
||||||
|
{
|
||||||
|
Width = 72, Height = 72,
|
||||||
|
Stretch = System.Windows.Media.Stretch.UniformToFill,
|
||||||
|
Source = bmp, ToolTip = System.IO.Path.GetFileName(path)
|
||||||
|
};
|
||||||
|
img.Clip = new System.Windows.Media.RectangleGeometry(new Rect(0, 0, 72, 72), 4, 4);
|
||||||
|
|
||||||
|
var removeBtn = new Button
|
||||||
|
{
|
||||||
|
Width = 18, Height = 18, Content = "\u2715",
|
||||||
|
FontSize = 11, FontWeight = FontWeights.Bold,
|
||||||
|
Cursor = Cursors.Hand, ToolTip = "Remove",
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(0, 2, 2, 0), Padding = new Thickness(0),
|
||||||
|
Background = new System.Windows.Media.SolidColorBrush(
|
||||||
|
System.Windows.Media.Color.FromArgb(200, 30, 30, 30)),
|
||||||
|
Foreground = System.Windows.Media.Brushes.White,
|
||||||
|
BorderThickness = new Thickness(0), Opacity = 0
|
||||||
|
};
|
||||||
|
removeBtn.Click += (s, ev) =>
|
||||||
|
{
|
||||||
|
ev.Handled = true;
|
||||||
|
_draft.PhotoPaths.Remove(path);
|
||||||
|
RebuildBPhotoThumbnails();
|
||||||
|
};
|
||||||
|
|
||||||
|
Border? coverBadge = null;
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
coverBadge = new Border
|
||||||
|
{
|
||||||
|
CornerRadius = new CornerRadius(3),
|
||||||
|
Background = new System.Windows.Media.SolidColorBrush(
|
||||||
|
System.Windows.Media.Color.FromArgb(210, 60, 90, 200)),
|
||||||
|
Padding = new Thickness(3, 1, 3, 1),
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
|
Margin = new Thickness(2, 2, 0, 0),
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "Cover", FontSize = 8, FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = System.Windows.Media.Brushes.White
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = new Grid
|
||||||
|
{
|
||||||
|
Width = 72, Height = 72, Margin = new Thickness(4),
|
||||||
|
Cursor = Cursors.SizeAll, AllowDrop = true, Tag = path
|
||||||
|
};
|
||||||
|
container.Children.Add(img);
|
||||||
|
if (coverBadge != null) container.Children.Add(coverBadge);
|
||||||
|
container.Children.Add(removeBtn);
|
||||||
|
|
||||||
|
container.MouseEnter += (s, ev) => removeBtn.Opacity = 1;
|
||||||
|
container.MouseLeave += (s, ev) => removeBtn.Opacity = 0;
|
||||||
|
|
||||||
|
Point dragStart = default;
|
||||||
|
bool isDragging = false;
|
||||||
|
container.MouseLeftButtonDown += (s, ev) => dragStart = ev.GetPosition(null);
|
||||||
|
container.MouseMove += (s, ev) =>
|
||||||
|
{
|
||||||
|
if (ev.LeftButton != MouseButtonState.Pressed || isDragging) return;
|
||||||
|
var pos = ev.GetPosition(null);
|
||||||
|
if (Math.Abs(pos.X - dragStart.X) > SystemParameters.MinimumHorizontalDragDistance ||
|
||||||
|
Math.Abs(pos.Y - dragStart.Y) > SystemParameters.MinimumVerticalDragDistance)
|
||||||
|
{
|
||||||
|
isDragging = true;
|
||||||
|
DragDrop.DoDragDrop(container, path, DragDropEffects.Move);
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
container.DragOver += (s, ev) =>
|
||||||
|
{
|
||||||
|
if (ev.Data.GetDataPresent(typeof(string)) &&
|
||||||
|
(string)ev.Data.GetData(typeof(string)) != path)
|
||||||
|
{ ev.Effects = DragDropEffects.Move; container.Opacity = 0.45; }
|
||||||
|
else ev.Effects = DragDropEffects.None;
|
||||||
|
ev.Handled = true;
|
||||||
|
};
|
||||||
|
container.DragLeave += (s, ev) => container.Opacity = 1.0;
|
||||||
|
container.Drop += (s, ev) =>
|
||||||
|
{
|
||||||
|
container.Opacity = 1.0;
|
||||||
|
if (!ev.Data.GetDataPresent(typeof(string))) return;
|
||||||
|
var src = (string)ev.Data.GetData(typeof(string));
|
||||||
|
var tgt = (string)container.Tag;
|
||||||
|
if (src == tgt) return;
|
||||||
|
var si = _draft.PhotoPaths.IndexOf(src);
|
||||||
|
var ti = _draft.PhotoPaths.IndexOf(tgt);
|
||||||
|
if (si < 0 || ti < 0) return;
|
||||||
|
_draft.PhotoPaths.RemoveAt(si);
|
||||||
|
_draft.PhotoPaths.Insert(ti, src);
|
||||||
|
RebuildBPhotoThumbnails();
|
||||||
|
ev.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
BPhotosPanel.Children.Add(container);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMorePhotos_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Add more photos",
|
||||||
|
Filter = "Images|*.jpg;*.jpeg;*.png;*.gif;*.webp;*.bmp|All files|*.*",
|
||||||
|
Multiselect = true
|
||||||
|
};
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
foreach (var path in dlg.FileNames)
|
||||||
|
{
|
||||||
|
if (_draft.PhotoPaths.Count >= MaxPhotos) break;
|
||||||
|
if (!_draft.PhotoPaths.Contains(path)) _draft.PhotoPaths.Add(path);
|
||||||
|
}
|
||||||
|
RebuildBPhotoThumbnails();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Footer actions ----
|
||||||
|
|
||||||
|
private void StartOver_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var isDirty = !string.IsNullOrWhiteSpace(BTitleBox.Text) ||
|
||||||
|
!string.IsNullOrWhiteSpace(BDescBox.Text);
|
||||||
|
if (isDirty)
|
||||||
|
{
|
||||||
|
var result = MessageBox.Show("Start over? Any edits will be lost.",
|
||||||
|
"Start Over", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||||
|
if (result != MessageBoxResult.OK) return;
|
||||||
|
}
|
||||||
|
ResetToStateA();
|
||||||
|
}
|
||||||
|
|
||||||
// Stub for ResetToStateA — implemented in Task 4
|
|
||||||
public void ResetToStateA()
|
public void ResetToStateA()
|
||||||
{
|
{
|
||||||
_photoPaths.Clear();
|
_photoPaths.Clear();
|
||||||
@@ -222,9 +610,94 @@ public partial class NewListingView : UserControl
|
|||||||
_lastAnalysis = null;
|
_lastAnalysis = null;
|
||||||
UpdateThumbStrip();
|
UpdateThumbStrip();
|
||||||
UpdateAnalyseButton();
|
UpdateAnalyseButton();
|
||||||
|
if (BPhotosPanel != null) BPhotosPanel.Children.Clear();
|
||||||
|
if (BTitleBox != null) BTitleBox.Text = "";
|
||||||
|
if (BDescBox != null) BDescBox.Text = "";
|
||||||
|
if (BCategoryBox != null) { BCategoryBox.Text = ""; BCategoryList.Visibility = Visibility.Collapsed; }
|
||||||
|
if (BCategoryIdLabel != null) BCategoryIdLabel.Text = "(no category selected)";
|
||||||
|
if (BPriceBox != null) BPriceBox.Value = 0;
|
||||||
|
if (BPriceHint != null) BPriceHint.Visibility = Visibility.Collapsed;
|
||||||
|
if (BConditionBox != null) BConditionBox.SelectedIndex = 3;
|
||||||
|
if (BFormatBox != null) BFormatBox.SelectedIndex = 0;
|
||||||
|
if (BPostcodeBox != null) BPostcodeBox.Text = _defaultPostcode;
|
||||||
SetState(ListingState.DropZone);
|
SetState(ListingState.DropZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void SaveDraft_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_savedService == null) return;
|
||||||
|
if (!ValidateDraft()) return;
|
||||||
|
CollectDraftFromFields();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_savedService.Save(
|
||||||
|
_draft.Title, _draft.Description, _draft.Price,
|
||||||
|
_draft.CategoryName, GetSelectedCondition().ToString(),
|
||||||
|
_draft.PhotoPaths);
|
||||||
|
GetWindow()?.RefreshSavedListings();
|
||||||
|
GetWindow()?.SetStatus($"Draft saved: {_draft.Title}");
|
||||||
|
SaveDraftBtn.IsEnabled = false;
|
||||||
|
await Task.Delay(600);
|
||||||
|
SaveDraftBtn.IsEnabled = true;
|
||||||
|
ResetToStateA();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError("Save Failed", ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Post_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_listingService == null) return;
|
||||||
|
if (!ValidateDraft()) return;
|
||||||
|
CollectDraftFromFields();
|
||||||
|
SetPostBusy(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = await _listingService.PostListingAsync(_draft);
|
||||||
|
_draft.EbayListingUrl = url;
|
||||||
|
var urlBox = FindName("BSuccessUrl") as TextBlock;
|
||||||
|
if (urlBox != null) urlBox.Text = url;
|
||||||
|
SetState(ListingState.Success);
|
||||||
|
GetWindow()?.SetStatus($"Listed: {_draft.Title}");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { ShowError("Post Failed", ex.Message); }
|
||||||
|
finally { SetPostBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CollectDraftFromFields()
|
||||||
|
{
|
||||||
|
_draft.Title = BTitleBox.Text.Trim();
|
||||||
|
_draft.Description = BDescBox.Text.Trim();
|
||||||
|
_draft.Price = (decimal)(BPriceBox.Value ?? 0);
|
||||||
|
_draft.Condition = GetSelectedCondition();
|
||||||
|
_draft.Format = BFormatBox.SelectedIndex == 0 ? ListingFormat.FixedPrice : ListingFormat.Auction;
|
||||||
|
_draft.Postcode = BPostcodeBox.Text;
|
||||||
|
_draft.Quantity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ValidateDraft()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(BTitleBox?.Text))
|
||||||
|
{ ShowError("Validation", "Please enter a title."); return false; }
|
||||||
|
if (BTitleBox.Text.Length > 80)
|
||||||
|
{ ShowError("Validation", "Title must be 80 characters or fewer."); return false; }
|
||||||
|
if (string.IsNullOrEmpty(_draft.CategoryId))
|
||||||
|
{ ShowError("Validation", "Please select a category."); return false; }
|
||||||
|
if ((BPriceBox?.Value ?? 0) <= 0)
|
||||||
|
{ ShowError("Validation", "Please enter a price greater than zero."); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPostBusy(bool busy)
|
||||||
|
{
|
||||||
|
PostBtn.IsEnabled = !busy;
|
||||||
|
PostSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
PostIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
IsEnabled = !busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowError(string title, string msg)
|
||||||
|
=> MessageBox.Show(msg, title, MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
|
||||||
private static bool IsImageFile(string path)
|
private static bool IsImageFile(string path)
|
||||||
{
|
{
|
||||||
var ext = System.IO.Path.GetExtension(path).ToLowerInvariant();
|
var ext = System.IO.Path.GetExtension(path).ToLowerInvariant();
|
||||||
|
|||||||
Reference in New Issue
Block a user