feat(confirmations): reveal items in grouped market sell confirmations (#18)

Grouped market sell confirmations previously appeared as a single row showing only the item count (e.g. "Sell 4 pcs"). This made it difficult to verify which items were being confirmed or their prices.

Add an expandable grouped confirmation that reveals individual items with their icon, name and price. Also allow confirming or canceling items individually while keeping confirm-all and cancel-all actions.

Includes a scrollable list for large batches and minor UI/code cleanup.

Co-authored-by: bohdanbtw [bohdanbtw@DESKTOP-ANGF2KJ](mailto:bohdanbtw@DESKTOP-ANGF2KJ)
This commit is contained in:
bohdanbtw
2026-03-12 15:27:03 +00:00
committed by GitHub
parent 053faec343
commit 4b547e19c4
6 changed files with 182 additions and 50 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using NebulaAuth.Model; using NebulaAuth.Model;
@@ -130,7 +130,7 @@ public static class DialogsController
var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg); var content = msg == null ? new ConfirmCancelDialog() : new ConfirmCancelDialog(msg);
var result = await DialogHost.Show(content); var result = await DialogHost.Show(content);
return result != null && (bool) result; return result != null && (bool)result;
} }
public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null) public static async Task<string?> ShowTextFieldDialog(string? title = null, string? msg = null)
+3 -3
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -51,7 +51,7 @@ public static class Storage
await Task.Run(() => await Task.Run(() =>
{ {
return Parallel.ForEachAsync(files, return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount}, new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = threadCount },
async (file, ct) => async (file, ct) =>
{ {
try try
@@ -218,7 +218,7 @@ public static class Storage
if (counter % 5 == 0) if (counter % 5 == 0)
{ {
progress?.Report(counter / (double) files.Count); progress?.Report(counter / (double)files.Count);
await Task.Delay(10); await Task.Delay(10);
} }
} }
+91 -32
View File
@@ -1,4 +1,4 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked" xmlns:confirmations="clr-namespace:SteamLib.SteamMobile.Confirmations;assembly=SteamLibForked"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
@@ -6,7 +6,6 @@
xmlns:entities="clr-namespace:NebulaAuth.Model.Entities" xmlns:entities="clr-namespace:NebulaAuth.Model.Entities"
xmlns:vm="clr-namespace:NebulaAuth.ViewModel"> xmlns:vm="clr-namespace:NebulaAuth.ViewModel">
<DataTemplate DataType="{x:Type confirmations:Confirmation}"> <DataTemplate DataType="{x:Type confirmations:Confirmation}">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -175,36 +174,96 @@
</Grid> </Grid>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}"> <DataTemplate DataType="{x:Type entities:MarketMultiConfirmation}">
<Grid> <Expander Padding="0" Background="Transparent" materialDesign:ExpanderAssist.ExpanderButtonPosition="Start" materialDesign:ExpanderAssist.HorizontalHeaderPadding="0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions> <Expander.Header>
<ColumnDefinition Width="Auto" /> <Grid HorizontalAlignment="Stretch">
<ColumnDefinition Width="*" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" />
<materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp" <ColumnDefinition Width="Auto" />
Margin="0,0,10,0" /> <ColumnDefinition Width="Auto" />
<TextBlock VerticalAlignment="Center" Grid.Column="1"> </Grid.ColumnDefinitions>
<Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" /> <materialDesign:PackIcon VerticalAlignment="Center" Width="20" Height="20" Kind="CartArrowUp"
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" /> Margin="0,0,10,0" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" /> <TextBlock VerticalAlignment="Center" Grid.Column="1">
</TextBlock> <Run Text="{Tr MainWindow.ConfirmationTemplates.Market, IsDynamic=False}" />
<Run Text="{Binding Confirmations.Count, Mode=OneWay}" />
<Run Text="{Tr Common.Abbreviations.Count.Items, IsDynamic=False}" />
</TextBlock>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left" Margin="5,0,0,0" <TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Center" Margin="5,0,0,0"
Text="{Binding Time, StringFormat=t}" /> Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}" <!-- Confirm all -->
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}" <Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="4"
CommandParameter="{Binding }"> Style="{StaticResource MaterialDesignIconButton}"
<materialDesign:PackIcon Kind="Check" /> ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmAll}"
</Button> Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}" CommandParameter="{Binding }">
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}" <materialDesign:PackIcon Kind="CheckAll" />
CommandParameter="{Binding }"> </Button>
<materialDesign:PackIcon Kind="Close" /> <!-- Cancel all -->
</Button> <Button Grid.Column="5" Style="{StaticResource MaterialDesignIconButton}"
</Grid> ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelAll}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</Expander.Header>
<Border Margin="30,6,0,0" Background="{DynamicResource MaterialDesignPaper}" CornerRadius="4">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
MaxHeight="320">
<ItemsControl ItemsSource="{Binding Confirmations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type confirmations:MarketConfirmation}">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="32"
MaxHeight="32" Margin="0,0,10,0"
Source="{Binding ItemImageUri}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock TextWrapping="WrapWithOverflow" Text="{Binding ItemName}" />
</StackPanel>
<TextBlock VerticalAlignment="Center" Grid.Column="2" HorizontalAlignment="Left"
Margin="8,0,0,0"
Text="{Binding Time, StringFormat=t}" />
<Button Margin="5,0,0,0" VerticalAlignment="Center" Grid.Column="3"
Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.ConfirmOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Check" />
</Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
ToolTip="{Tr MainWindow.ConfirmationTemplates.CancelOne}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Close" />
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Expander>
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}"> <DataTemplate DataType="{x:Type confirmations:PurchaseConfirmation}">
<Grid> <Grid>
@@ -227,7 +286,7 @@
<materialDesign:PackIcon Kind="Check" /> <materialDesign:PackIcon Kind="Check" />
</Button> </Button>
<Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}" <Button Grid.Column="4" Style="{StaticResource MaterialDesignIconButton}"
Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.CancelCommand)}" Command="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.(vm:MainVM.ConfirmCommand)}"
CommandParameter="{Binding }"> CommandParameter="{Binding }">
<materialDesign:PackIcon Kind="Close" /> <materialDesign:PackIcon Kind="Close" />
</Button> </Button>
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
@@ -76,18 +76,27 @@ public partial class MainVM //Confirmations
return SendConfirmation(SelectedMafile, confirmation, false); return SendConfirmation(SelectedMafile, confirmation, false);
} }
private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm) private async Task SendConfirmation(Mafile mafile, Confirmation confirmation, bool confirm)
{ {
bool result; bool result;
try try
{ {
if (confirmation is MarketMultiConfirmation multi) switch (confirmation)
{ {
result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm); case MarketMultiConfirmation multi:
} result = await MaClient.SendMultipleConfirmation(mafile, multi.Confirmations, confirm);
else break;
{
result = await MaClient.SendConfirmation(mafile, confirmation, confirm); case MarketConfirmation market:
result = await MaClient.SendConfirmation(mafile, market, confirm);
break;
default:
result = await MaClient.SendConfirmation(mafile, confirmation, confirm);
break;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -96,13 +105,49 @@ public partial class MainVM //Confirmations
return; return;
} }
if (result) if (!result)
{
Confirmations.Remove(confirmation);
}
else
{ {
SnackbarController.SendSnackbar(GetLocalization("ConfirmationError")); SnackbarController.SendSnackbar(GetLocalization("ConfirmationError"));
return;
} }
UpdateConfirmationState(confirmation);
}
private void UpdateConfirmationState(Confirmation confirmation)
{
if (confirmation is MarketConfirmation item)
{
var parent = Confirmations
.OfType<MarketMultiConfirmation>()
.FirstOrDefault(p => p.Confirmations.Contains(item));
if (parent == null)
{
Confirmations.Remove(item);
return;
}
parent.Confirmations.Remove(item);
if (parent.Confirmations.Count == 0)
{
Confirmations.Remove(parent);
return;
}
if (parent.Confirmations.Count == 1)
{
var remaining = parent.Confirmations[0];
var idx = Confirmations.IndexOf(parent);
Confirmations[idx] = remaining;
return;
}
parent.Time = parent.Confirmations.FirstOrDefault()?.Time ?? parent.Time;
return;
}
Confirmations.Remove(confirmation);
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
+28
View File
@@ -410,6 +410,34 @@
"zh": "购买", "zh": "购买",
"ua": "Покупка", "ua": "Покупка",
"fr": "Acheter" "fr": "Acheter"
},
"ConfirmAll": {
"en": "Confirm all",
"ru": "Подтвердить все",
"zh": "全部确认",
"ua": "Підтвердити все",
"fr": "Tout confirmer"
},
"CancelAll": {
"en": "Cancel all",
"ru": "Отклонить все",
"zh": "全部取消",
"ua": "Відхилити все",
"fr": "Tout annuler"
},
"ConfirmOne": {
"en": "Confirm this item",
"ru": "Подтвердить этот предмет",
"zh": "确认此物品",
"ua": "Підтвердити цей предмет",
"fr": "Confirmer cet objet"
},
"CancelOne": {
"en": "Cancel this item",
"ru": "Отклонить этот предмет",
"zh": "取消此物品",
"ua": "Відхилити цей предмет",
"fr": "Annuler cet objet"
} }
}, },
"CodeCopied": { "CodeCopied": {