mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-26 12:09:32 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
560d87bf67 | ||
|
|
c3911fd5bf | ||
|
|
ef27310e4f | ||
|
|
9ea1c83342 | ||
|
|
bbcee67fe0 | ||
|
|
4706561d42 | ||
|
|
c9d5fe37bb | ||
|
|
6bca078614 | ||
|
|
a4481011eb | ||
|
|
81d39b5436 | ||
|
|
4bff8602d1 | ||
|
|
4e434d25fe | ||
|
|
4e2cfc23ae |
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
run_path=$(dirname "$0")
|
||||
base_path="$(dirname "$run_path")"
|
||||
mkdir -p "$base_path"
|
||||
cd "$base_path" || exit 1
|
||||
appVer_path="$base_path/WattToolkit.AppVer"
|
||||
exec_name="Steam++"
|
||||
tar_name="WattToolkit.tgz"
|
||||
tar_path="$base_path/$tar_name"
|
||||
app_name="Watt Toolkit"
|
||||
PROCESS_NAMES=("$exec_name" "$app_name")
|
||||
export LC_ALL=en_US.UTF-8
|
||||
Install_certutil() {
|
||||
# 判断发行版类型
|
||||
if command -v certutil &>/dev/null; then
|
||||
echo "certutil 工具已安装。"
|
||||
else
|
||||
echo "证书导入以及验证需要使用 certutil 工具。"
|
||||
# 判断包管理器
|
||||
if command -v apt &>/dev/null; then
|
||||
# 使用 apt (Debian/Ubuntu)
|
||||
sudo apt update
|
||||
sudo apt install -y libnss3-tools
|
||||
elif command -v dnf &>/dev/null; then
|
||||
# 使用 dnf (Fedora)
|
||||
sudo dnf install -y nss-tools
|
||||
elif command -v yum &>/dev/null; then
|
||||
# 使用 yum (CentOS/Red Hat)
|
||||
sudo yum install -y nss-tools
|
||||
elif command -v pacman &>/dev/null; then
|
||||
# 使用 pacman (Arch Linux)
|
||||
# sudo pacman -S nss
|
||||
echo "请手动安装 certutil 工具。"
|
||||
exit 1
|
||||
else
|
||||
echo "请手动安装 certutil 工具。"
|
||||
exit 1
|
||||
fi
|
||||
echo "certutil 工具已安装。"
|
||||
fi
|
||||
}
|
||||
certutil_Init() {
|
||||
certutil -d $HOME/.pki/nssdb -L
|
||||
ret=$?
|
||||
if [ $ret -ne 0 ]; then
|
||||
echo "即将初始化 certutil \$HOME/.pki/nssdb"
|
||||
mkdir -p $HOME/.pki
|
||||
mkdir -p $HOME/.pki/nssdb
|
||||
chmod 700 $HOME/.pki/nssdb
|
||||
certutil -d $HOME/.pki/nssdb -N --empty-password
|
||||
else
|
||||
echo "certutil nssdb 正常"
|
||||
fi
|
||||
}
|
||||
Install_jq() {
|
||||
# Check if jq is already installed
|
||||
if command -v jq &>/dev/null; then
|
||||
echo "jq 工具已安装。"
|
||||
else
|
||||
echo "jq 用来解析版本更新。"
|
||||
# Check the package manager
|
||||
if command -v apt &>/dev/null; then
|
||||
# Using apt (Debian/Ubuntu)
|
||||
sudo apt update
|
||||
sudo apt install -y jq
|
||||
elif command -v dnf &>/dev/null; then
|
||||
# Using dnf (Fedora)
|
||||
sudo dnf install -y jq
|
||||
elif command -v yum &>/dev/null; then
|
||||
# Using yum (CentOS/Red Hat)
|
||||
sudo yum install -y jq
|
||||
elif command -v pacman &>/dev/null; then
|
||||
# Using pacman (Arch Linux)
|
||||
sudo pacman -S jq
|
||||
else
|
||||
echo "请手动安装 jq 工具。"
|
||||
exit 1
|
||||
fi
|
||||
echo "请手动安装 jq 工具。"
|
||||
fi
|
||||
}
|
||||
Install_zenity() {
|
||||
# 判断发行版类型
|
||||
if command -v zenity &>/dev/null; then
|
||||
echo "zenity 工具已安装。"
|
||||
else
|
||||
echo "安装过程需要 zenity 工具。"
|
||||
# 判断包管理器
|
||||
if command -v apt &>/dev/null; then
|
||||
# 使用 apt (Debian/Ubuntu)
|
||||
sudo apt update
|
||||
sudo apt install -y zenity
|
||||
elif command -v dnf &>/dev/null; then
|
||||
# 使用 dnf (Fedora)
|
||||
sudo dnf install -y zenity
|
||||
elif command -v yum &>/dev/null; then
|
||||
# 使用 yum (CentOS/Red Hat)
|
||||
sudo yum install -y zenity
|
||||
elif command -v pacman &>/dev/null; then
|
||||
# 使用 pacman (Arch Linux)
|
||||
# sudo pacman -S zenity
|
||||
echo "请手动安装 zenity 工具。"
|
||||
exit 1
|
||||
else
|
||||
echo "请手动安装 zenity 工具。"
|
||||
exit 1
|
||||
fi
|
||||
echo "zenity 工具已安装。"
|
||||
fi
|
||||
}
|
||||
|
||||
Show_Run() {
|
||||
local param1=$1
|
||||
# 显示提示框,询问是否运行程序
|
||||
zenity --question --text="$1" --width=400
|
||||
|
||||
# 获取上一个命令的退出码
|
||||
response=$?
|
||||
|
||||
if [ $response -eq 0 ]; then
|
||||
echo "程序已启动。"
|
||||
exit 0 &
|
||||
# 用户点击了 "运行" 按钮,启动程序
|
||||
/bin/sh -c "$base_path/$exec_name.sh"
|
||||
else
|
||||
# 用户点击了 "关闭" 按钮,退出脚本
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
Kill_Process() {
|
||||
# 尝试的次数
|
||||
Kill_MAX_RETRIES=3
|
||||
|
||||
# 循环尝试终止进程
|
||||
for process_name in "${PROCESS_NAMES[@]}"; do
|
||||
kill_retry=1
|
||||
while [ $kill_retry -le $Kill_MAX_RETRIES ]; do
|
||||
pid=$(pgrep "$process_name")
|
||||
if [ -n "$pid" ]; then
|
||||
echo "尝试 $kill_retry: 进程 $process_name 正在运行中。正在终止..."
|
||||
kill $pid
|
||||
sleep 2
|
||||
else
|
||||
break
|
||||
fi
|
||||
kill_retry=$((kill_retry + 1))
|
||||
done
|
||||
done
|
||||
|
||||
for process_name in "${PROCESS_NAMES[@]}"; do
|
||||
# 检查是否成功终止进程
|
||||
if pgrep -x "$process_name" >/dev/null; then
|
||||
echo "无法终止程序 $process_name。尝试次数已达上限。"
|
||||
exit 1
|
||||
else
|
||||
echo "程序 $process_name 已成功终止。"
|
||||
fi
|
||||
done
|
||||
|
||||
}
|
||||
Install_certutil
|
||||
Install_zenity
|
||||
Install_jq
|
||||
certutil_Init
|
||||
Kill_Process
|
||||
|
||||
dotnet_exec="$dotnet_path/dotnet/dotnet"
|
||||
if [ -x "$dotnet_exec" ]; then
|
||||
echo "文件具有执行权限。"
|
||||
else
|
||||
chmod +x "$dotnet_exec"
|
||||
fi
|
||||
chmod +x "$base_path/$exec_name.sh"
|
||||
# xdg-icon-resource install "$base_path/Icons/Watt-Toolkit.png" --size 128 Watt-Toolkit
|
||||
#添加桌面文件
|
||||
rm -rf "$HOME/Desktop/Watt Toolkit.desktop" 2>/dev/null
|
||||
echo "#!/usr/bin/env xdg-open
|
||||
[Desktop Entry]
|
||||
Name=Watt Toolkit
|
||||
Exec=$base_path/$exec_name.sh
|
||||
Icon=$base_path/Icons/Watt-Toolkit.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
StartupNotify=false" >"$HOME/Desktop/Watt Toolkit.desktop"
|
||||
chmod +x "$HOME/Desktop/Watt Toolkit.desktop"
|
||||
# update-desktop-database ~/.local/share/applications
|
||||
#运行程序
|
||||
Show_Run "下载安装完成,是否启动程序?"
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
base_path="$HOME/WattToolkit"
|
||||
export LC_ALL=en_US.UTF-8
|
||||
run_path=$(dirname "$0")
|
||||
base_path="$(dirname "$run_path")"
|
||||
exec_name="Steam++"
|
||||
app_name="Watt Toolkit"
|
||||
AppData="$XDG_DATA_HOME/Steam++"
|
||||
@@ -8,7 +8,37 @@ AppData_t="$HOME/.local/share/Steam++"
|
||||
Cache="$XDG_CACHE_HOME/Steam++"
|
||||
Cache_t="$HOME/.cache/Steam+"
|
||||
PROCESS_NAMES=("$exec_name" "$app_name")
|
||||
export LC_ALL=zn_CN.UTF-8
|
||||
#检查是否存在 Steam++.sh 避免卸载错误
|
||||
file_to_check="$base_path/Steam++.sh"
|
||||
folders_to_check=("dotnet" "modules" "native" "script" "assemblies")
|
||||
if [ -e "$file_to_check" ]; then
|
||||
echo "文件 $file_to_check 存在"
|
||||
else
|
||||
echo "文件 $file_to_check 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for folder in "${folders_to_check[@]}"; do
|
||||
folder_path="$base_path/$folder"
|
||||
if [ -d "$folder_path" ]; then
|
||||
echo "文件夹 $folder_path 存在"
|
||||
else
|
||||
echo "文件夹 $folder_path 不存在"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "验证该文件夹是 $app_name 安装目录,请确认是否卸载。"
|
||||
|
||||
read -p "所有文件和文件夹都存在,确认卸载吗? (输入 'y' 确认): " confirmation
|
||||
|
||||
if [ "${confirmation,,}" == "y" ]; then
|
||||
echo "开始执行卸载操作"
|
||||
# 在这里添加您的卸载操作
|
||||
else
|
||||
echo "用户取消卸载操作"
|
||||
exit 0
|
||||
fi
|
||||
Kill_Process() {
|
||||
# 尝试的次数
|
||||
Kill_MAX_RETRIES=3
|
||||
|
||||
@@ -981,7 +981,7 @@
|
||||
{
|
||||
"TypeName": "IdleRule",
|
||||
"PropertyName": "IdleRule",
|
||||
"DefaultValue": "IdleRule.OnlyOneGame",
|
||||
"DefaultValue": "IdleRule.FastMode",
|
||||
"DefaultValueIsConst": false,
|
||||
"Summary": "运行规则",
|
||||
"IsRegionOrEndregion": null,
|
||||
|
||||
+1
-1
Submodule ref/DirectoryPackages updated: 07bf9c2d47...0a1c04d40d
+1
-1
Submodule ref/SteamClient updated: ebf61ea6a3...2d79c7da61
Submodule ref/WTTS.MicroServices.ClientSDK updated: 24bf5ab24b...be6ab5fda9
+1
-1
Submodule ref/WinAuth updated: a20460165f...29806fdcf8
@@ -126,6 +126,12 @@
|
||||
<ProjectReference Include="..\BD.WTTS.Client\BD.WTTS.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="UI\Views\Controls\Widgets\CarouselItems.cs">
|
||||
<DependentUpon>%(Filename)</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\TFM_NETX_WITH_DESKTOP.props" />
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace BD.WTTS;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerable"/> extensions methods
|
||||
/// </summary>
|
||||
public static class IEnumerableExtensions
|
||||
{
|
||||
public static IEnumerable<IEnumerable> Batch(this IEnumerable source, int size)
|
||||
{
|
||||
object[]? bucket = null;
|
||||
var count = 0;
|
||||
|
||||
foreach (var item in source)
|
||||
{
|
||||
bucket ??= new object[size];
|
||||
|
||||
bucket[count++] = item;
|
||||
|
||||
if (count == size)
|
||||
{
|
||||
yield return bucket;
|
||||
bucket = null;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (bucket != null && count > 0)
|
||||
yield return bucket.Take(count).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ public sealed partial class App : Application
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
#if WINDOWS || LINUX || MACOS
|
||||
if (GeneralSettings.MinimizeOnStartup.Value)
|
||||
//非主窗口不执行启动时最小化逻辑
|
||||
if (GeneralSettings.MinimizeOnStartup.Value && MainWindow is MainWindow)
|
||||
Startup.Instance.IsMinimize = true;
|
||||
if (Startup.Instance.IsSteamRun)
|
||||
{
|
||||
@@ -101,7 +102,7 @@ public sealed partial class App : Application
|
||||
|
||||
if (Startup.Instance.IsMinimize && MainWindow is AppWindow appWindow)
|
||||
{
|
||||
Task2.InBackground(() => appWindow.SplashScreen.RunTasks(CancellationToken.None));
|
||||
Task2.InBackground(() => appWindow.SplashScreen?.RunTasks(CancellationToken.None));
|
||||
}
|
||||
}
|
||||
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
<Thickness x:Key="PageMargin">10</Thickness>
|
||||
<sys:Double x:Key="SettingsSpacing">3</sys:Double>
|
||||
|
||||
<!--<sys:Char x:Key="PasswordChar">●</sys:Char>-->
|
||||
<sys:Char x:Key="PasswordChar">•</sys:Char>
|
||||
<sys:Char x:Key="PasswordChar">●</sys:Char>
|
||||
<!--<sys:Char x:Key="PasswordChar">•</sys:Char>
|
||||
<sys:Char x:Key="PasswordChar">·</sys:Char>-->
|
||||
<FontFamily x:Key="ContentControlThemeFontFamily">Null</FontFamily>
|
||||
<BoxShadows x:Key="InfoBoxShadow">4 4 32 0 #66000000</BoxShadows>
|
||||
|
||||
@@ -44,6 +45,7 @@
|
||||
|
||||
<StyleInclude Source="/UI/Styling/InfoBox.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/PageBase.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/CarouselItems.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/OptionsDisplayItem.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/RippleEffect.axaml" />
|
||||
<StyleInclude Source="/UI/Styling/Controls/Draggable.axaml" />
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<Styles
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:generic="using:System.Collections.Generic"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
xmlns:sys="using:System"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
x:CompileBindings="True">
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20">
|
||||
<spp:CarouselItems>
|
||||
<spp:CarouselItems.Items>
|
||||
<Border Background="Red" />
|
||||
<Border Background="Blue" />
|
||||
<Border Background="Black" />
|
||||
<Border Background="Yellow" />
|
||||
</spp:CarouselItems.Items>
|
||||
</spp:CarouselItems>
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Style Selector="spp|CarouselItems">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="8">
|
||||
<Panel>
|
||||
<Carousel Name="CarouselControl" Classes="AnimeSlide">
|
||||
<Carousel.ItemTemplate>
|
||||
<DataTemplate DataType="sys:Object">
|
||||
<ItemsControl ItemTemplate="{Binding $parent[spp:CarouselItems].ItemTemplate}" ItemsSource="{Binding}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<spp:FixedWrapPanel ItemsPerLine="{TemplateBinding ItemsPerPage}" Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</Carousel.ItemTemplate>
|
||||
</Carousel>
|
||||
<Button
|
||||
Name="Left"
|
||||
HorizontalAlignment="Left"
|
||||
Classes="action"
|
||||
IsVisible="False"
|
||||
Theme="{StaticResource TransparentButton}">
|
||||
<ui:SymbolIcon FontSize="14" Symbol="ChevronLeft" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="Right"
|
||||
HorizontalAlignment="Right"
|
||||
Classes="action"
|
||||
IsVisible="False"
|
||||
Theme="{StaticResource TransparentButton}">
|
||||
<ui:SymbolIcon FontSize="14" Symbol="ChevronRight" />
|
||||
</Button>
|
||||
|
||||
<Panel
|
||||
Height="10"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom">
|
||||
<ItemsRepeater Name="Swiper" IsVisible="False">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout
|
||||
DisableVirtualization="True"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DataTemplate.DataType>
|
||||
<x:Type x:TypeArguments="x:Int32, x:String" TypeName="generic:KeyValuePair" />
|
||||
</DataTemplate.DataType>
|
||||
<Button
|
||||
Padding="0"
|
||||
Classes="Plain"
|
||||
Command="{Binding $parent[spp:CarouselItems].CarouselBannerIndexCommand}"
|
||||
CommandParameter="{Binding Key}">
|
||||
<Ellipse
|
||||
Name="IndexEllipse"
|
||||
Width="10"
|
||||
Height="10"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Fill="{Binding Value}"
|
||||
Opacity="0.6"
|
||||
StrokeThickness="0" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Panel>
|
||||
</Panel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
|
||||
<Style Selector="spp|CarouselItems /template/ Button.action">
|
||||
<Setter Property="Margin" Value="4" />
|
||||
<Setter Property="Width" Value="36" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public enum AlignContent
|
||||
{
|
||||
FlexStart,
|
||||
FlexEnd,
|
||||
Center,
|
||||
Stretch,
|
||||
SpaceBetween,
|
||||
SpaceAround,
|
||||
SpaceEvenly
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
[SuppressMessage("Naming", "CA1717:Only FlagsAttribute enums should have plural names")]
|
||||
public enum AlignItems
|
||||
{
|
||||
FlexStart,
|
||||
FlexEnd,
|
||||
Center,
|
||||
Stretch
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public static class Flex
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an attached property to control the alignment of a specific child in a flex layout.
|
||||
/// </summary>
|
||||
public static readonly AttachedProperty<AlignItems?> AlignSelfProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, AlignItems?>("AlignSelf", typeof(Flex));
|
||||
|
||||
/// <summary>
|
||||
/// Defines an attached property to control the order of a specific child in a flex layout.
|
||||
/// </summary>
|
||||
public static readonly AttachedProperty<int> OrderProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, int>("Order", typeof(Flex));
|
||||
|
||||
public static readonly AttachedProperty<FlexBasis> BasisProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, FlexBasis>("Basis", typeof(Flex), FlexBasis.Auto);
|
||||
|
||||
public static readonly AttachedProperty<double> ShrinkProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, double>("Shrink", typeof(Flex), 1.0, validate: v => v >= 0.0);
|
||||
|
||||
public static readonly AttachedProperty<double> GrowProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, double>("Grow", typeof(Flex), 0.0, validate: v => v >= 0.0);
|
||||
|
||||
internal static readonly AttachedProperty<double> BaseLengthProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, double>("BaseLength", typeof(Flex), 0.0);
|
||||
|
||||
internal static readonly AttachedProperty<double> CurrentLengthProperty =
|
||||
AvaloniaProperty.RegisterAttached<Layoutable, double>("CurrentLength", typeof(Flex), 0.0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child alignment in a flex layout
|
||||
/// </summary>
|
||||
public static AlignItems? GetAlignSelf(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(AlignSelfProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the child alignment in a flex layout
|
||||
/// </summary>
|
||||
public static void SetAlignSelf(Layoutable layoutable, AlignItems? value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(AlignSelfProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child order in a flex layout
|
||||
/// </summary>
|
||||
public static int GetOrder(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(OrderProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the child order in a flex layout
|
||||
/// </summary>
|
||||
public static void SetOrder(Layoutable layoutable, int value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(OrderProperty, value);
|
||||
}
|
||||
|
||||
public static FlexBasis GetBasis(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(BasisProperty);
|
||||
}
|
||||
|
||||
public static void SetBasis(Layoutable layoutable, FlexBasis value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(BasisProperty, value);
|
||||
}
|
||||
|
||||
public static double GetShrink(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(ShrinkProperty);
|
||||
}
|
||||
|
||||
public static void SetShrink(Layoutable layoutable, double value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(ShrinkProperty, value);
|
||||
}
|
||||
|
||||
public static double GetGrow(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(GrowProperty);
|
||||
}
|
||||
|
||||
public static void SetGrow(Layoutable layoutable, double value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(GrowProperty, value);
|
||||
}
|
||||
|
||||
internal static double GetBaseLength(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(BaseLengthProperty);
|
||||
}
|
||||
|
||||
internal static void SetBaseLength(Layoutable layoutable, double value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(BaseLengthProperty, value);
|
||||
}
|
||||
|
||||
internal static double GetCurrentLength(Layoutable layoutable)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
return layoutable.GetValue(CurrentLengthProperty);
|
||||
}
|
||||
|
||||
internal static void SetCurrentLength(Layoutable layoutable, double value)
|
||||
{
|
||||
if (layoutable is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(layoutable));
|
||||
}
|
||||
|
||||
layoutable.SetValue(CurrentLengthProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public readonly struct FlexBasis : IEquatable<FlexBasis>
|
||||
{
|
||||
public double Value { get; }
|
||||
|
||||
public FlexBasisKind Kind { get; }
|
||||
|
||||
public FlexBasis(double value, FlexBasisKind kind)
|
||||
{
|
||||
if (value < 0 || double.IsNaN(value) || double.IsInfinity(value))
|
||||
throw new ArgumentException($"Invalid basis value: {value}", nameof(value));
|
||||
if (kind < FlexBasisKind.Auto || kind > FlexBasisKind.Relative)
|
||||
throw new ArgumentException($"Invalid basis kind: {kind}", nameof(kind));
|
||||
Value = value;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public FlexBasis(double value) : this(value, FlexBasisKind.Absolute) { }
|
||||
|
||||
public static FlexBasis Auto => new(0.0, FlexBasisKind.Auto);
|
||||
|
||||
public bool IsAuto => Kind == FlexBasisKind.Auto;
|
||||
|
||||
public bool IsAbsolute => Kind == FlexBasisKind.Absolute;
|
||||
|
||||
public bool IsRelative => Kind == FlexBasisKind.Relative;
|
||||
|
||||
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
|
||||
public bool Equals(FlexBasis other) =>
|
||||
(IsAuto && other.IsAuto) || (Value == other.Value && Kind == other.Kind);
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is FlexBasis other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Value, (int)Kind);
|
||||
|
||||
public static bool operator ==(FlexBasis left, FlexBasis right) =>
|
||||
left.Equals(right);
|
||||
|
||||
public static bool operator !=(FlexBasis left, FlexBasis right) =>
|
||||
!left.Equals(right);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Kind switch
|
||||
{
|
||||
FlexBasisKind.Auto => "Auto",
|
||||
FlexBasisKind.Absolute => FormattableString.Invariant($"{Value:G17}"),
|
||||
FlexBasisKind.Relative => FormattableString.Invariant($"{Value * 100:G17}%"),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static FlexBasis Parse(string str)
|
||||
{
|
||||
return str.ToUpperInvariant() switch
|
||||
{
|
||||
"AUTO" => Auto,
|
||||
var s when s.EndsWith("%") => new FlexBasis(ParseDouble(s[..^1].Trim()) / 100, FlexBasisKind.Relative),
|
||||
_ => new FlexBasis(ParseDouble(str), FlexBasisKind.Absolute),
|
||||
};
|
||||
double ParseDouble(string s) => double.Parse(s, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public enum FlexBasisKind
|
||||
{
|
||||
Auto,
|
||||
Absolute,
|
||||
Relative,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public enum FlexDirection
|
||||
{
|
||||
Row,
|
||||
RowReverse,
|
||||
Column,
|
||||
ColumnReverse
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public sealed class FlexPanel : Panel
|
||||
{
|
||||
private static readonly Func<Layoutable, int> s_getOrder = x => x is { } y ? Flex.GetOrder(y) : 0;
|
||||
private static readonly Func<Layoutable, bool> s_isVisible = x => x.IsVisible;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Direction"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FlexDirection> DirectionProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, FlexDirection>(nameof(Direction));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="JustifyContent"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<JustifyContent> JustifyContentProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, JustifyContent>(nameof(JustifyContent));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="AlignItems"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<AlignItems> AlignItemsProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, AlignItems>(nameof(AlignItems));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="AlignContent"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<AlignContent> AlignContentProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, AlignContent>(nameof(AlignContent));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="Wrap"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<FlexWrap> WrapProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, FlexWrap>(nameof(Wrap), FlexWrap.Wrap);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ColumnSpacing"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> ColumnSpacingProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, double>(nameof(ColumnSpacing));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="RowSpacing"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<double> RowSpacingProperty =
|
||||
AvaloniaProperty.Register<FlexPanel, double>(nameof(RowSpacing));
|
||||
|
||||
private FlexLayoutState? _state;
|
||||
|
||||
static FlexPanel()
|
||||
{
|
||||
AffectsMeasure<FlexPanel>(
|
||||
DirectionProperty,
|
||||
JustifyContentProperty,
|
||||
WrapProperty,
|
||||
ColumnSpacingProperty,
|
||||
RowSpacingProperty);
|
||||
|
||||
AffectsArrange<FlexPanel>(
|
||||
AlignItemsProperty,
|
||||
AlignContentProperty);
|
||||
|
||||
AffectsParentMeasure<FlexPanel>(
|
||||
HorizontalAlignmentProperty,
|
||||
VerticalAlignmentProperty,
|
||||
Flex.OrderProperty,
|
||||
Flex.BasisProperty,
|
||||
Flex.ShrinkProperty,
|
||||
Flex.GrowProperty);
|
||||
|
||||
AffectsParentArrange<FlexPanel>(
|
||||
Flex.AlignSelfProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flex direction
|
||||
/// </summary>
|
||||
public FlexDirection Direction
|
||||
{
|
||||
get => GetValue(DirectionProperty);
|
||||
set => SetValue(DirectionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flex justify content mode
|
||||
/// </summary>
|
||||
public JustifyContent JustifyContent
|
||||
{
|
||||
get => GetValue(JustifyContentProperty);
|
||||
set => SetValue(JustifyContentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flex align items mode
|
||||
/// </summary>
|
||||
public AlignItems AlignItems
|
||||
{
|
||||
get => GetValue(AlignItemsProperty);
|
||||
set => SetValue(AlignItemsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flex align content mode
|
||||
/// </summary>
|
||||
public AlignContent AlignContent
|
||||
{
|
||||
get => GetValue(AlignContentProperty);
|
||||
set => SetValue(AlignContentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flex wrap mode
|
||||
/// </summary>
|
||||
public FlexWrap Wrap
|
||||
{
|
||||
get => GetValue(WrapProperty);
|
||||
set => SetValue(WrapProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the column spacing
|
||||
/// </summary>
|
||||
public double ColumnSpacing
|
||||
{
|
||||
get => GetValue(ColumnSpacingProperty);
|
||||
set => SetValue(ColumnSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the row spacing
|
||||
/// </summary>
|
||||
public double RowSpacing
|
||||
{
|
||||
get => GetValue(RowSpacingProperty);
|
||||
set => SetValue(RowSpacingProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var children = (IReadOnlyList<Layoutable>)Children;
|
||||
|
||||
var isColumn = Direction is FlexDirection.Column or FlexDirection.ColumnReverse;
|
||||
|
||||
var max = Uv.FromSize(availableSize, isColumn);
|
||||
var spacing = Uv.FromSize(ColumnSpacing, RowSpacing, isColumn);
|
||||
|
||||
var (lineU, lineV, lineShrink, lineGrow, lineAutoMargins) = (0.0, 0.0, 0.0, 0.0, 0);
|
||||
var (childIndex, firstChildIndex, itemIndex, lineIndex) = (0, 0, 0, 0);
|
||||
|
||||
var lines = new List<FlexLine>();
|
||||
children = children.Where(s_isVisible).OrderBy(s_getOrder).ToArray();
|
||||
|
||||
foreach (var element in children)
|
||||
{
|
||||
var basis = Flex.GetBasis(element);
|
||||
var flexConstraint = basis.Kind switch
|
||||
{
|
||||
FlexBasisKind.Auto => max.U,
|
||||
FlexBasisKind.Absolute => basis.Value,
|
||||
FlexBasisKind.Relative => max.U * basis.Value / 100,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
element.Measure(Uv.ToSize(max.WithU(flexConstraint), isColumn));
|
||||
|
||||
var size = Uv.FromSize(element.DesiredSize, isColumn);
|
||||
var flexLength = basis.Kind switch
|
||||
{
|
||||
FlexBasisKind.Auto => size.U,
|
||||
FlexBasisKind.Absolute or FlexBasisKind.Relative => Math.Max(size.U, flexConstraint),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
size = size.WithU(flexLength);
|
||||
Flex.SetBaseLength(element, flexLength);
|
||||
Flex.SetCurrentLength(element, flexLength);
|
||||
|
||||
if (Wrap != FlexWrap.NoWrap && lineU + size.U + itemIndex * spacing.U > max.U)
|
||||
{
|
||||
lines.Add(new FlexLine(firstChildIndex, childIndex - 1,
|
||||
lineU, lineV, lineShrink, lineGrow, lineAutoMargins));
|
||||
(lineU, lineV, lineShrink, lineGrow, lineAutoMargins) = (0.0, 0.0, 0.0, 0.0, 0);
|
||||
firstChildIndex = childIndex;
|
||||
itemIndex = 0;
|
||||
lineIndex++;
|
||||
}
|
||||
|
||||
lineU += size.U;
|
||||
lineV = Math.Max(lineV, size.V);
|
||||
lineShrink += Flex.GetShrink(element);
|
||||
lineGrow += Flex.GetGrow(element);
|
||||
lineAutoMargins += GetItemAutoMargins(element, isColumn);
|
||||
itemIndex++;
|
||||
childIndex++;
|
||||
}
|
||||
|
||||
if (itemIndex != 0)
|
||||
{
|
||||
lines.Add(new FlexLine(firstChildIndex, firstChildIndex + itemIndex - 1,
|
||||
lineU, lineV, lineShrink, lineGrow, lineAutoMargins));
|
||||
}
|
||||
|
||||
var state = new FlexLayoutState(children, lines, Wrap);
|
||||
|
||||
var totalSpacingV = (lines.Count - 1) * spacing.V;
|
||||
var panelSizeU = lines.Count > 0 ? lines.Max(line => line.U + (line.Count - 1) * spacing.U) : 0.0;
|
||||
|
||||
// Reizing along main axis using grow and shrink factors can affect cross axis, so remeasure affected items and lines.
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var (itemsCount, totalSpacingU, totalU, freeU) = GetLineMeasureU(line, max.U, spacing.U);
|
||||
var (lineMult, autoMargins, remainingFreeU) = GetLineMultInfo(line, freeU);
|
||||
if (lineMult != 0.0 && remainingFreeU != 0.0)
|
||||
{
|
||||
foreach (var element in state.GetLineItems(line))
|
||||
{
|
||||
var baseLength = Flex.GetBaseLength(element);
|
||||
var mult = GetItemMult(element, freeU);
|
||||
if (mult != 0.0)
|
||||
{
|
||||
var length = Math.Max(0.0, baseLength + remainingFreeU * mult / lineMult);
|
||||
element.Measure(Uv.ToSize(max.WithU(length), isColumn));
|
||||
}
|
||||
}
|
||||
|
||||
line.V = state.GetLineItems(line).Max(i => Uv.FromSize(i.DesiredSize, isColumn).V);
|
||||
}
|
||||
}
|
||||
|
||||
_state = state;
|
||||
var totalLineV = lines.Sum(l => l.V);
|
||||
var panelSize = lines.Count == 0 ? default : new Uv(panelSizeU, totalLineV + totalSpacingV);
|
||||
return Uv.ToSize(panelSize, isColumn);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var state = _state ?? throw new InvalidOperationException();
|
||||
|
||||
var isColumn = Direction is FlexDirection.Column or FlexDirection.ColumnReverse;
|
||||
var isReverse = Direction is FlexDirection.RowReverse or FlexDirection.ColumnReverse;
|
||||
|
||||
var panelSize = Uv.FromSize(finalSize, isColumn);
|
||||
var spacing = Uv.FromSize(ColumnSpacing, RowSpacing, isColumn);
|
||||
|
||||
var linesCount = state.Lines.Count;
|
||||
var totalLineV = state.Lines.Sum(s => s.V);
|
||||
var totalSpacingV = (linesCount - 1) * spacing.V;
|
||||
var totalV = totalLineV + totalSpacingV;
|
||||
var freeV = panelSize.V - totalV;
|
||||
|
||||
var alignContent = freeV >= 0.0 ? AlignContent : AlignContent switch
|
||||
{
|
||||
AlignContent.FlexStart or AlignContent.Stretch or AlignContent.SpaceBetween => AlignContent.FlexStart,
|
||||
AlignContent.Center or AlignContent.SpaceAround or AlignContent.SpaceEvenly => AlignContent.Center,
|
||||
AlignContent.FlexEnd => AlignContent.FlexEnd,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
var (spacingV, v) = alignContent switch
|
||||
{
|
||||
AlignContent.FlexStart => (spacing.V, 0.0),
|
||||
AlignContent.FlexEnd => (spacing.V, freeV),
|
||||
AlignContent.Center => (spacing.V, freeV / 2),
|
||||
AlignContent.Stretch => (spacing.V, 0.0),
|
||||
AlignContent.SpaceBetween => (spacing.V + freeV / (linesCount - 1), 0.0),
|
||||
AlignContent.SpaceAround => (spacing.V + freeV / linesCount, freeV / linesCount / 2),
|
||||
AlignContent.SpaceEvenly => (spacing.V + freeV / (linesCount + 1), freeV / (linesCount + 1)),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
var scaleV = alignContent == AlignContent.Stretch ? (panelSize.V - totalSpacingV) / totalLineV : 1.0;
|
||||
|
||||
foreach (var line in state.Lines)
|
||||
{
|
||||
var lineV = scaleV * line.V;
|
||||
var (itemsCount, totalSpacingU, totalU, freeU) = GetLineMeasureU(line, panelSize.U, spacing.U);
|
||||
var (lineMult, lineAutoMargins, remainingFreeU) = GetLineMultInfo(line, freeU);
|
||||
|
||||
var currentFreeU = remainingFreeU;
|
||||
if (lineMult != 0.0 && remainingFreeU != 0.0)
|
||||
{
|
||||
foreach (var element in state.GetLineItems(line))
|
||||
{
|
||||
var baseLength = Flex.GetBaseLength(element);
|
||||
var mult = GetItemMult(element, freeU);
|
||||
if (mult != 0.0)
|
||||
{
|
||||
var length = Math.Max(0.0, baseLength + remainingFreeU * mult / lineMult);
|
||||
Flex.SetCurrentLength(element, length);
|
||||
currentFreeU -= length - baseLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
remainingFreeU = currentFreeU;
|
||||
|
||||
if (lineAutoMargins != 0 && remainingFreeU != 0.0)
|
||||
{
|
||||
foreach (var element in state.GetLineItems(line))
|
||||
{
|
||||
var baseLength = Flex.GetCurrentLength(element);
|
||||
var autoMargins = GetItemAutoMargins(element, isColumn);
|
||||
if (autoMargins != 0)
|
||||
{
|
||||
var length = Math.Max(0.0, baseLength + remainingFreeU * autoMargins / lineAutoMargins);
|
||||
Flex.SetCurrentLength(element, length);
|
||||
currentFreeU -= length - baseLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
remainingFreeU = currentFreeU;
|
||||
|
||||
var (spacingU, u) = line.Grow > 0 ? (spacing.U, 0.0) : JustifyContent switch
|
||||
{
|
||||
JustifyContent.FlexStart => (spacing.U, 0.0),
|
||||
JustifyContent.FlexEnd => (spacing.U, remainingFreeU),
|
||||
JustifyContent.Center => (spacing.U, remainingFreeU / 2),
|
||||
JustifyContent.SpaceBetween => (spacing.U + remainingFreeU / (itemsCount - 1), 0.0),
|
||||
JustifyContent.SpaceAround => (spacing.U + remainingFreeU / itemsCount, remainingFreeU / itemsCount / 2),
|
||||
JustifyContent.SpaceEvenly => (spacing.U + remainingFreeU / (itemsCount + 1), remainingFreeU / (itemsCount + 1)),
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
foreach (var element in state.GetLineItems(line))
|
||||
{
|
||||
var size = Uv.FromSize(element.DesiredSize, isColumn).WithU(Flex.GetCurrentLength(element));
|
||||
var align = Flex.GetAlignSelf(element) ?? AlignItems;
|
||||
|
||||
var positionV = align switch
|
||||
{
|
||||
AlignItems.FlexStart => v,
|
||||
AlignItems.FlexEnd => v + lineV - size.V,
|
||||
AlignItems.Center => v + (lineV - size.V) / 2,
|
||||
AlignItems.Stretch => v,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
size = size.WithV(align == AlignItems.Stretch ? lineV : size.V);
|
||||
var position = new Uv(isReverse ? panelSize.U - size.U - u : u, positionV);
|
||||
element.Arrange(new Rect(Uv.ToPoint(position, isColumn), Uv.ToSize(size, isColumn)));
|
||||
|
||||
u += size.U + spacingU;
|
||||
}
|
||||
|
||||
v += lineV + spacingV;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private static (int ItemsCount, double TotalSpacingU, double TotalU, double FreeU) GetLineMeasureU(
|
||||
FlexLine line, double panelSizeU, double spacingU)
|
||||
{
|
||||
var itemsCount = line.Count;
|
||||
var totalSpacingU = (itemsCount - 1) * spacingU;
|
||||
var totalU = line.U + totalSpacingU;
|
||||
var freeU = panelSizeU - totalU;
|
||||
return (itemsCount, totalSpacingU, totalU, freeU);
|
||||
}
|
||||
|
||||
private static (double LineMult, double LineAutoMargins, double RemainingFreeU) GetLineMultInfo(FlexLine line, double freeU)
|
||||
{
|
||||
var lineMult = freeU switch
|
||||
{
|
||||
< 0 => line.Shrink,
|
||||
> 0 => line.Grow,
|
||||
_ => 0.0,
|
||||
};
|
||||
// https://www.w3.org/TR/css-flexbox-1/#remaining-free-space
|
||||
// Sum of flex factors less than 1 reduces remaining free space to be distributed.
|
||||
return lineMult is > 0 and < 1
|
||||
? (lineMult, line.AutoMargins, freeU * lineMult)
|
||||
: (lineMult, line.AutoMargins, freeU);
|
||||
}
|
||||
|
||||
private static double GetItemMult(Layoutable element, double freeU)
|
||||
{
|
||||
var mult = freeU switch
|
||||
{
|
||||
< 0 => Flex.GetShrink(element),
|
||||
> 0 => Flex.GetGrow(element),
|
||||
_ => 0.0,
|
||||
};
|
||||
return mult;
|
||||
}
|
||||
|
||||
private static int GetItemAutoMargins(Layoutable element, bool isColumn)
|
||||
{
|
||||
return isColumn
|
||||
? element.VerticalAlignment switch
|
||||
{
|
||||
VerticalAlignment.Stretch => 0,
|
||||
VerticalAlignment.Top or VerticalAlignment.Bottom => 1,
|
||||
VerticalAlignment.Center => 2,
|
||||
_ => throw new InvalidOperationException()
|
||||
}
|
||||
: element.HorizontalAlignment switch
|
||||
{
|
||||
HorizontalAlignment.Stretch => 0,
|
||||
HorizontalAlignment.Left or HorizontalAlignment.Right => 1,
|
||||
HorizontalAlignment.Center => 2,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
}
|
||||
|
||||
private readonly struct FlexLayoutState
|
||||
{
|
||||
private readonly IReadOnlyList<Layoutable> _children;
|
||||
|
||||
public IReadOnlyList<FlexLine> Lines { get; }
|
||||
|
||||
public FlexLayoutState(IReadOnlyList<Layoutable> children, List<FlexLine> lines, FlexWrap wrap)
|
||||
{
|
||||
if (wrap == FlexWrap.WrapReverse)
|
||||
{
|
||||
lines.Reverse();
|
||||
}
|
||||
_children = children;
|
||||
Lines = lines;
|
||||
}
|
||||
|
||||
public IEnumerable<Layoutable> GetLineItems(FlexLine line)
|
||||
{
|
||||
for (var i = line.First; i <= line.Last; i++)
|
||||
yield return _children[i];
|
||||
}
|
||||
}
|
||||
|
||||
private class FlexLine
|
||||
{
|
||||
public FlexLine(int first, int last, double u, double v, double shrink, double grow, int autoMargins)
|
||||
{
|
||||
First = first;
|
||||
Last = last;
|
||||
U = u;
|
||||
V = v;
|
||||
Shrink = shrink;
|
||||
Grow = grow;
|
||||
AutoMargins = autoMargins;
|
||||
}
|
||||
|
||||
/// <summary>First item index.</summary>
|
||||
public int First { get; }
|
||||
|
||||
/// <summary>Last item index.</summary>
|
||||
public int Last { get; }
|
||||
|
||||
/// <summary>Sum of main sizes of items.</summary>
|
||||
public double U { get; }
|
||||
|
||||
/// <summary>Max of cross sizes of items.</summary>
|
||||
public double V { get; set; }
|
||||
|
||||
/// <summary>Sum of shrink factors of flexible items.</summary>
|
||||
public double Shrink { get; }
|
||||
|
||||
/// <summary>Sum of grow factors of flexible items.</summary>
|
||||
public double Grow { get; }
|
||||
|
||||
/// <summary>Number of "auto margins" along main axis.</summary>
|
||||
public int AutoMargins { get; }
|
||||
|
||||
/// <summary>Number of items.</summary>
|
||||
public int Count => Last - First + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public enum FlexWrap
|
||||
{
|
||||
NoWrap,
|
||||
Wrap,
|
||||
WrapReverse
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public enum JustifyContent
|
||||
{
|
||||
FlexStart,
|
||||
FlexEnd,
|
||||
Center,
|
||||
SpaceBetween,
|
||||
SpaceAround,
|
||||
SpaceEvenly
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
internal readonly struct Uv(double u, double v)
|
||||
{
|
||||
public double U { get; } = u;
|
||||
|
||||
public double V { get; } = v;
|
||||
|
||||
public static Uv FromSize(double width, double height, bool swap) =>
|
||||
new Uv(swap ? height : width, swap ? width : height);
|
||||
|
||||
public static Uv FromSize(Size size, bool swap) =>
|
||||
FromSize(size.Width, size.Height, swap);
|
||||
|
||||
public static Point ToPoint(Uv uv, bool swap) =>
|
||||
new Point(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
|
||||
|
||||
public static Size ToSize(Uv uv, bool swap) =>
|
||||
new Size(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
|
||||
|
||||
public Uv WithU(double u) =>
|
||||
new Uv(u, V);
|
||||
|
||||
public Uv WithV(double v) =>
|
||||
new Uv(U, v);
|
||||
|
||||
public override string ToString() =>
|
||||
$"U: {U}, V: {V}";
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace BD.WTTS.UI.Views.Controls
|
||||
/// Defines the <see cref="AutoScrollInterval"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> AutoScrollIntervalProperty =
|
||||
AvaloniaProperty.Register<CarouselBanner, int>(nameof(AutoScrollInterval), 8000);
|
||||
AvaloniaProperty.Register<CarouselBanner, int>(nameof(AutoScrollInterval), 6000);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Avalonia.Controls.ItemsControl.Items property.
|
||||
@@ -85,8 +85,8 @@ namespace BD.WTTS.UI.Views.Controls
|
||||
public CarouselBanner()
|
||||
{
|
||||
InitializeComponent();
|
||||
Left.Click += (s, e) => SwiperPrevious();
|
||||
Right.Click += (s, e) => SwiperNext();
|
||||
Left.Command = ReactiveCommand.Create(SwiperPrevious);
|
||||
Right.Command = ReactiveCommand.Create(SwiperNext);
|
||||
|
||||
//CarouselControl[!Carousel.Items] = this[!Items];
|
||||
CarouselControl[!Carousel.ItemsSourceProperty] = this[!ItemsSourceProperty];
|
||||
@@ -96,7 +96,7 @@ namespace BD.WTTS.UI.Views.Controls
|
||||
.Subscribe(_ => SwipersLoad());
|
||||
|
||||
CarouselControl.GetObservable(Carousel.SelectedIndexProperty)
|
||||
.Subscribe(x => SwipersLoad());
|
||||
.Subscribe(_ => SwipersLoad());
|
||||
|
||||
this.GetObservable(AutoScrollProperty)
|
||||
.Subscribe(x =>
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using FluentAvalonia.Core;
|
||||
using System.Reactive;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public class CarouselItems : ItemsControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="AutoScroll"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<bool> AutoScrollProperty =
|
||||
AvaloniaProperty.Register<CarouselItems, bool>(nameof(AutoScroll), true);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="AutoScrollInterval"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> AutoScrollIntervalProperty =
|
||||
AvaloniaProperty.Register<CarouselItems, int>(nameof(AutoScrollInterval), 6000);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ItemsPerPage"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<int> ItemsPerPageProperty =
|
||||
AvaloniaProperty.Register<CarouselItems, int>(nameof(ItemsPerPage), 4);
|
||||
|
||||
/// <summary>
|
||||
/// AutoScroll
|
||||
/// </summary>
|
||||
public bool AutoScroll
|
||||
{
|
||||
get => GetValue(AutoScrollProperty);
|
||||
set => SetValue(AutoScrollProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动播放间隔时间(单位毫秒)
|
||||
/// </summary>
|
||||
public int AutoScrollInterval
|
||||
{
|
||||
get => GetValue(AutoScrollIntervalProperty);
|
||||
set => SetValue(AutoScrollIntervalProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ItemsPerPage
|
||||
/// </summary>
|
||||
public int ItemsPerPage
|
||||
{
|
||||
get => GetValue(ItemsPerPageProperty);
|
||||
set => SetValue(ItemsPerPageProperty, value);
|
||||
}
|
||||
|
||||
private Carousel? _carouselControl;
|
||||
private Button? _leftButton;
|
||||
private Button? _rightButton;
|
||||
private ItemsRepeater? _swiper;
|
||||
private Timer? _timer;
|
||||
|
||||
public ICommand? CarouselBannerIndexCommand { get; }
|
||||
|
||||
public CarouselItems()
|
||||
{
|
||||
CarouselBannerIndexCommand = ReactiveCommand.Create<int>(CarouselBannerIndex);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
_carouselControl = e.NameScope.Find<Carousel>("CarouselControl");
|
||||
_leftButton = e.NameScope.Find<Button>("Left");
|
||||
_rightButton = e.NameScope.Find<Button>("Right");
|
||||
_swiper = e.NameScope.Find<ItemsRepeater>("Swiper");
|
||||
|
||||
if (_leftButton != null)
|
||||
{
|
||||
_leftButton.Command = ReactiveCommand.Create(SwiperPrevious);
|
||||
}
|
||||
if (_rightButton != null)
|
||||
{
|
||||
_rightButton.Command = ReactiveCommand.Create(SwiperNext);
|
||||
}
|
||||
|
||||
if (_carouselControl != null)
|
||||
{
|
||||
_carouselControl.GetObservable(Carousel.ItemCountProperty)
|
||||
.Subscribe(_ => SwipersLoad());
|
||||
|
||||
_carouselControl.GetObservable(Carousel.SelectedIndexProperty)
|
||||
.Subscribe(_ => SwipersLoad());
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (change.Property == ItemCountProperty)
|
||||
{
|
||||
RefreshItemsSource();
|
||||
}
|
||||
else if (change.Property == ItemsPerPageProperty)
|
||||
{
|
||||
RefreshItemsSource();
|
||||
}
|
||||
else if (change.Property == AutoScrollProperty)
|
||||
{
|
||||
var x = change.GetNewValue<bool>();
|
||||
if (x && _timer == null)
|
||||
{
|
||||
_timer = new Timer(_ =>
|
||||
{
|
||||
if (!this.IsPointerOver)
|
||||
{
|
||||
Dispatcher.UIThread.Post(SwiperNext, DispatcherPriority.Background);
|
||||
}
|
||||
}, nameof(AutoScroll), AutoScrollInterval, AutoScrollInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
void RefreshItemsSource()
|
||||
{
|
||||
if (_carouselControl != null)
|
||||
{
|
||||
var items = ItemsSource?.Batch(ItemsPerPage);
|
||||
_carouselControl.ItemsSource = items;
|
||||
}
|
||||
}
|
||||
|
||||
private void CarouselBannerIndex(int index)
|
||||
{
|
||||
if (_carouselControl != null)
|
||||
_carouselControl.SelectedIndex = index;
|
||||
}
|
||||
|
||||
private void SwipersLoad()
|
||||
{
|
||||
if (_carouselControl == null || _swiper == null || _leftButton == null || _rightButton == null)
|
||||
return;
|
||||
|
||||
if (_carouselControl.ItemCount <= 0)
|
||||
{
|
||||
_swiper.ItemsSource = null;
|
||||
return;
|
||||
}
|
||||
if (_carouselControl.ItemCount == 1)
|
||||
{
|
||||
_leftButton.IsVisible = _rightButton.IsVisible = _swiper.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_leftButton.IsVisible = _rightButton.IsVisible = _swiper.IsVisible = true;
|
||||
var arr = new Dictionary<int, string>();
|
||||
for (var i = 0; i < _carouselControl.ItemCount; i++)
|
||||
{
|
||||
arr.Add(i, "#ADADAD");
|
||||
}
|
||||
var index = _carouselControl.SelectedIndex < 0 ? 0 : _carouselControl.SelectedIndex;
|
||||
arr[index] = "#FFFFFF";
|
||||
|
||||
_swiper.ItemsSource = arr;
|
||||
}
|
||||
}
|
||||
|
||||
private void SwiperNext()
|
||||
{
|
||||
if (_carouselControl == null || _carouselControl.ItemCount < 1)
|
||||
return;
|
||||
|
||||
if (_carouselControl.SelectedIndex < _carouselControl.ItemCount - 1)
|
||||
{
|
||||
_carouselControl.Next();
|
||||
}
|
||||
else
|
||||
{
|
||||
_carouselControl.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void SwiperPrevious()
|
||||
{
|
||||
if (_carouselControl == null || _carouselControl.ItemCount < 1)
|
||||
return;
|
||||
|
||||
if (_carouselControl.SelectedIndex > 0)
|
||||
{
|
||||
_carouselControl.Previous();
|
||||
}
|
||||
else
|
||||
{
|
||||
_carouselControl.SelectedIndex = _carouselControl.ItemCount - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,26 @@ public class FixedWrapPanel : Panel, INavigableContainer
|
||||
public static readonly StyledProperty<int> ItemsPerLineProperty =
|
||||
AvaloniaProperty.Register<FixedWrapPanel, int>(nameof(ItemsPerLine), 3);
|
||||
|
||||
public static readonly StyledProperty<double> SpacingProperty =
|
||||
AvaloniaProperty.Register<FixedWrapPanel, double>(nameof(Spacing), 0);
|
||||
|
||||
static FixedWrapPanel()
|
||||
{
|
||||
AffectsMeasure<FixedWrapPanel>(ItemsPerLineProperty);
|
||||
}
|
||||
|
||||
public double ItemsPerLine
|
||||
public int ItemsPerLine
|
||||
{
|
||||
get => GetValue(ItemsPerLineProperty);
|
||||
set => SetValue(ItemsPerLineProperty, value);
|
||||
}
|
||||
|
||||
public double Spacing
|
||||
{
|
||||
get => GetValue(SpacingProperty);
|
||||
set => SetValue(SpacingProperty, value);
|
||||
}
|
||||
|
||||
IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
|
||||
{
|
||||
int index = from is not null ? Children.IndexOf((Control)from) : -1;
|
||||
@@ -63,7 +72,7 @@ public class FixedWrapPanel : Panel, INavigableContainer
|
||||
MutableSize currentLineSize = default;
|
||||
MutableSize panelSize = default;
|
||||
Size lineConstraint = new(constraint.Width, constraint.Height);
|
||||
Size childConstraint = new(itemWidth, constraint.Height);
|
||||
Size childConstraint = new(itemWidth - Spacing, constraint.Height);
|
||||
|
||||
for (int i = 0, count = Children.Count; i < count; i++)
|
||||
{
|
||||
@@ -76,7 +85,7 @@ public class FixedWrapPanel : Panel, INavigableContainer
|
||||
child.Measure(childConstraint);
|
||||
Size childSize = new(itemWidth, child.DesiredSize.Height);
|
||||
|
||||
if (MathUtilities.GreaterThan(currentLineSize.Width + childSize.Width, lineConstraint.Width))
|
||||
if (MathUtilities.GreaterThan(currentLineSize.Width + childSize.Width + Spacing, lineConstraint.Width))
|
||||
{
|
||||
// Need to switch to another line
|
||||
panelSize.Width = Max(currentLineSize.Width, panelSize.Width);
|
||||
@@ -86,14 +95,17 @@ public class FixedWrapPanel : Panel, INavigableContainer
|
||||
else
|
||||
{
|
||||
// Continue to accumulate a line
|
||||
currentLineSize.Width += childSize.Width;
|
||||
currentLineSize.Width += childSize.Width + Spacing;
|
||||
currentLineSize.Height = Max(childSize.Height, currentLineSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
// The last line size, if any should be added
|
||||
panelSize.Width = Max(currentLineSize.Width, panelSize.Width);
|
||||
panelSize.Height += currentLineSize.Height;
|
||||
panelSize.Height = currentLineSize.Height;
|
||||
|
||||
if (double.IsInfinity(panelSize.Width))
|
||||
panelSize.Width = 0;
|
||||
|
||||
return panelSize.ToSize();
|
||||
}
|
||||
@@ -150,8 +162,8 @@ public class FixedWrapPanel : Panel, INavigableContainer
|
||||
continue;
|
||||
}
|
||||
|
||||
child.Arrange(new Rect(x, y, width, height));
|
||||
x += width;
|
||||
child.Arrange(new Rect(x, y, width - Spacing, height));
|
||||
x += width + Spacing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</spp:PageBase.PreviewImage>
|
||||
<spp:PageBase.TabContent>
|
||||
<TabStrip
|
||||
Name="TabSelect"
|
||||
Margin="4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
@@ -110,19 +111,45 @@
|
||||
</DropDownButton>
|
||||
</StackPanel>
|
||||
</spp:PageBase.ActionContent>
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<TextBox
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Classes="readonlybox"
|
||||
Text="{Binding DebugString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox
|
||||
Name="CommandTextBox"
|
||||
Grid.Row="1"
|
||||
Watermark="{Binding Path=Res.Debug_InstructionPlaceholder, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Carousel SelectedIndex="{Binding #TabSelect.SelectedIndex}">
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{DynamicResource ControlCornerRadius}">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<TextBox
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Classes="readonlybox"
|
||||
Text="{Binding DebugString}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox
|
||||
Name="CommandTextBox"
|
||||
Grid.Row="1"
|
||||
Watermark="{Binding Path=Res.Debug_InstructionPlaceholder, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Panel>
|
||||
<TextBlock Text="Test Control" />
|
||||
<ItemsControl>
|
||||
<ItemsControl.Items>
|
||||
<Border Height="30" Background="Red" />
|
||||
<Border Background="Blue" />
|
||||
<Border Background="Yellow" />
|
||||
<Border Height="30" Background="Yellow" />
|
||||
<Border Background="Red" />
|
||||
<Border Background="Blue" />
|
||||
</ItemsControl.Items>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<spp:FixedWrapPanel ItemsPerLine="3" Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</Panel>
|
||||
|
||||
<Panel>
|
||||
<TextBlock Text="Test Control" />
|
||||
</Panel>
|
||||
</Carousel>
|
||||
</spp:PageBase>
|
||||
|
||||
@@ -270,7 +270,68 @@
|
||||
Theme="{StaticResource HyperlinkButton}" />
|
||||
</DockPanel>
|
||||
|
||||
<spp:CarouselBanner
|
||||
<spp:CarouselItems
|
||||
MinHeight="250"
|
||||
ItemsPerPage="4"
|
||||
ItemsSource="{Binding Shops}">
|
||||
<spp:CarouselItems.ItemTemplate>
|
||||
<DataTemplate x:DataType="spp:ShopRecommendGoodItem">
|
||||
<Border spp:Animations.EnableAnimations="True">
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MaxWidth="260"
|
||||
Classes="Vertical Scale"
|
||||
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
|
||||
Cursor="Hand"
|
||||
Tags="{Binding GoodsAllCategorysText}"
|
||||
ToolTip.Tip="{Binding Brief}">
|
||||
<spp:AppItem.ClickCommandParameter>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding Path="." Source="{x:Static spp:HomePageViewModel.WattGame_Goods_Detail_}" />
|
||||
<CompiledBinding Path="Id" />
|
||||
</MultiBinding>
|
||||
</spp:AppItem.ClickCommandParameter>
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2 RenderOptions.BitmapInterpolationMode="HighQuality" Source="{Binding Image}" />
|
||||
</spp:AppItem.Image>
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{Binding !!DiscountValue}"
|
||||
Text="{Binding Mktprice, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}"
|
||||
TextDecorations="Strikethrough" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Price, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<spp:AppItem.ActionButton>
|
||||
<Border
|
||||
Height="24"
|
||||
Padding="8,0"
|
||||
Background="#4c6b22"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
IsVisible="{Binding !!DiscountValue}">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#beee11"
|
||||
Text="{Binding DiscountValue, StringFormat={}-{0}%}" />
|
||||
</Border>
|
||||
</spp:AppItem.ActionButton>
|
||||
</spp:AppItem>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</spp:CarouselItems.ItemTemplate>
|
||||
</spp:CarouselItems>
|
||||
|
||||
<!--<spp:CarouselBanner
|
||||
MinHeight="250"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
@@ -354,7 +415,8 @@
|
||||
ContentTemplate="{StaticResource ShopItem}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<!--<DataTemplate>
|
||||
-->
|
||||
<!--<DataTemplate>
|
||||
<DataTemplate.DataType>
|
||||
<x:Type x:TypeArguments="spp:ShopRecommendGoodItem" TypeName="col:IEnumerable" />
|
||||
</DataTemplate.DataType>
|
||||
@@ -366,8 +428,9 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</DataTemplate>-->
|
||||
<!--
|
||||
</spp:CarouselBanner.ItemTemplate>
|
||||
</spp:CarouselBanner>
|
||||
</spp:CarouselBanner>-->
|
||||
|
||||
<!--<ScrollViewer
|
||||
Name="ShopsScrollViewer"
|
||||
|
||||
@@ -170,13 +170,14 @@ public sealed class AppSplashScreen : IApplicationSplashScreen
|
||||
#pragma warning disable SA1114 // Parameter list should follow declaration
|
||||
IViewModelManager.Instance.InitViewModels(new TabItemViewModel[]
|
||||
{
|
||||
new MenuTabItemViewModel("Welcome")
|
||||
{
|
||||
PageType = typeof(HomePage),
|
||||
IsResourceGet = true,
|
||||
IconKey = "avares://BD.WTTS.Client.Avalonia/UI/Assets/Icons/home.ico",
|
||||
},
|
||||
}, ImmutableArray.Create<TabItemViewModel>(
|
||||
new MenuTabItemViewModel("Welcome")
|
||||
{
|
||||
PageType = typeof(HomePage),
|
||||
IsResourceGet = true,
|
||||
IconKey = "avares://BD.WTTS.Client.Avalonia/UI/Assets/Icons/home.ico",
|
||||
},
|
||||
},
|
||||
ImmutableArray.Create<TabItemViewModel>(
|
||||
#if DEBUG
|
||||
new MenuTabItemViewModel("Debug")
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ sealed class AcceleratorAutoMapperProfile : Profile
|
||||
.ForMember(x => x.Icon, d => d.MapFrom(x => x.IconUrl))
|
||||
.ForMember(x => x.Description, d => d.MapFrom(x => x.Describe))
|
||||
.ForMember(x => x.Enable, d => d.MapFrom(x => !x.Disable))
|
||||
.ForMember(x => x.IsBuild, d => d.MapFrom(x => x.IsCompile))
|
||||
.ForMember(x => x.Id, d => d.MapFrom(x => x.LocalId));
|
||||
CreateMap<Script, ScriptDTO>()
|
||||
.ForMember(x => x.LocalId, d => d.MapFrom(x => x.Id))
|
||||
@@ -18,6 +19,7 @@ sealed class AcceleratorAutoMapperProfile : Profile
|
||||
.ForMember(x => x.Describe, d => d.MapFrom(x => x.Description))
|
||||
.ForMember(x => x.IconUrl, d => d.MapFrom(x => x.Icon))
|
||||
.ForMember(x => x.Disable, d => d.MapFrom(x => !x.Enable))
|
||||
.ForMember(x => x.IsCompile, d => d.MapFrom(x => x.IsBuild))
|
||||
.ForMember(x => x.Id, d => d.MapFrom(x => x.Pid));
|
||||
}
|
||||
}
|
||||
@@ -376,20 +376,6 @@ public sealed class ScriptManager : GeneralHttpClientFactory, IScriptManager
|
||||
var scripts = list.ToList();
|
||||
foreach (var item in list)
|
||||
{
|
||||
//检查缓存文件夹如果不是 IScriptManager.DirName_Build 替换成 IScriptManager.DirName_Build 开头
|
||||
if (!item.CachePath.StartsWith($"{IScriptManager.DirName_Build}{Path.DirectorySeparatorChar}"))
|
||||
{
|
||||
item.CachePath = Path.Combine(IScriptManager.DirName_Build, item.FileName!);
|
||||
var cachePath = Path.Combine(Plugin.Instance.CacheDirectory, item.CachePath);
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
File.Delete(cachePath);
|
||||
}
|
||||
await TryReadFileAsync(item, true);
|
||||
//清理 脚本内容 后续 IPC 传递 插件进程读取。
|
||||
item.Content = string.Empty;
|
||||
await scriptRepository.SaveScriptCachePathAsync(item, default);
|
||||
}
|
||||
if (!CheckFile(item))
|
||||
{
|
||||
var temp = await DeleteScriptAsync(item);
|
||||
@@ -404,6 +390,38 @@ public sealed class ScriptManager : GeneralHttpClientFactory, IScriptManager
|
||||
//toast.Show($"脚本:{item.Name}_文件丢失,删除失败去尝试手动删除");
|
||||
Toast.Show(ToastIcon.Error, Strings.Script_NoFileDeleteError_.Format(item.Name));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
//检查缓存文件夹如果不是 IScriptManager.DirName_Build 替换成 IScriptManager.DirName_Build 开头
|
||||
if (!item.CachePath.StartsWith($"{IScriptManager.DirName_Build}{Path.DirectorySeparatorChar}"))
|
||||
{
|
||||
var oldCachePath = Path.Combine(Plugin.Instance.CacheDirectory, item.CachePath);
|
||||
item.CachePath = Path.Combine(IScriptManager.DirName_Build, item.FileName!);
|
||||
if (File.Exists(oldCachePath))
|
||||
{
|
||||
File.Delete(oldCachePath);
|
||||
}
|
||||
await TryReadFileAsync(item, true);
|
||||
//清理 脚本内容 后续 IPC 传递 插件进程读取。
|
||||
item.Content = string.Empty;
|
||||
await scriptRepository.SaveScriptCachePathAsync(item, default);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cachePath = Path.Combine(Plugin.Instance.CacheDirectory, item.CachePath);
|
||||
var file = new FileInfo(cachePath);
|
||||
//检查缓存文件是否为空 为空则 刷新
|
||||
if (file.Exists)
|
||||
{
|
||||
if (file.Length == 0)
|
||||
{
|
||||
file.Delete();
|
||||
await TryReadFileAsync(item, true);
|
||||
//清理 脚本内容 后续 IPC 传递 插件进程读取。
|
||||
item.Content = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return scripts;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<UserControl
|
||||
x:Class="BD.WTTS.UI.Views.Controls.GameAccelerator"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:spp="https://steampp.net/ui"
|
||||
d:DesignHeight="250"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
|
||||
<AutoCompleteBox MinWidth="200" Watermark="搜索游戏" />
|
||||
<Button Content="Left" />
|
||||
<Button Content="Right" />
|
||||
</StackPanel>
|
||||
<spp:CarouselItems Grid.Row="1">
|
||||
<spp:CarouselItems.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="test" />
|
||||
</DataTemplate>
|
||||
</spp:CarouselItems.ItemTemplate>
|
||||
</spp:CarouselItems>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace BD.WTTS.UI.Views.Controls;
|
||||
|
||||
public partial class GameAccelerator : UserControl
|
||||
{
|
||||
public GameAccelerator()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@
|
||||
<Panel>
|
||||
<lvc:CartesianChart
|
||||
x:Name="Chart"
|
||||
MinWidth="400"
|
||||
MinHeight="160"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent" />
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using LiveChartsCore;
|
||||
using LiveChartsCore.Motion;
|
||||
using LiveChartsCore.SkiaSharpView;
|
||||
using LiveChartsCore.SkiaSharpView.Avalonia;
|
||||
using LiveChartsCore.SkiaSharpView.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -49,39 +52,55 @@ public partial class ProxyChartView : UserControl
|
||||
}
|
||||
};
|
||||
|
||||
//public ISeries[] Series { get; set; }
|
||||
|
||||
private readonly List<RateTick> writes = new();
|
||||
private readonly List<RateTick> reads = new();
|
||||
private readonly ObservableCollection<RateTick> writes = new();
|
||||
private readonly ObservableCollection<RateTick> reads = new();
|
||||
|
||||
public Func<double, string> XFormatter { get; } = timestamp => ((long)timestamp).ToDateTimeS().ToString("HH:mm:ss");
|
||||
|
||||
public Func<double, string> YFormatter { get; } = value => $"{IOPath.GetDisplayFileSizeString(value)}/s";
|
||||
|
||||
CancellationTokenSource cancellation = new();
|
||||
|
||||
public static readonly AvaloniaProperty<IEnumerable<ISeries>> SeriesProperty =
|
||||
AvaloniaProperty.Register<CartesianChart, IEnumerable<ISeries>>("Series", Enumerable.Empty<ISeries>(), inherits: true);
|
||||
|
||||
public ObservableCollection<ISeries> Series
|
||||
{
|
||||
get
|
||||
{
|
||||
return (ObservableCollection<ISeries>)GetValue(SeriesProperty);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
SetValue(SeriesProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public ProxyChartView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.readSeries.Values = reads;
|
||||
this.writeSeries.Values = writes;
|
||||
Series = [readSeries, writeSeries];
|
||||
|
||||
if (Chart != null)
|
||||
{
|
||||
//Chart.UpdateFinished += Chart_UpdateFinished;
|
||||
Chart.Series = new ISeries[] { readSeries, writeSeries };
|
||||
//Chart.Series = new ISeries[] { readSeries, writeSeries };
|
||||
Chart[!CartesianChart.SeriesProperty] = this[!SeriesProperty];
|
||||
Chart.XAxes = new Axis[] { new Axis { Labeler = XFormatter } };
|
||||
Chart.YAxes = new Axis[] { new Axis { Labeler = YFormatter, MinLimit = 0 } };
|
||||
}
|
||||
|
||||
CancellationTokenSource? cancellation = null;
|
||||
|
||||
ProxyService.Current.WhenAnyValue(x => x.ProxyStatus)
|
||||
.Subscribe(x =>
|
||||
{
|
||||
if (x)
|
||||
{
|
||||
cancellation = new CancellationTokenSource();
|
||||
FlushFlowChartAsync(cancellation.Token);
|
||||
Task2.InBackground(FlushFlowChartAsync, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -96,9 +115,7 @@ public partial class ProxyChartView : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
private static double GetTimestamp(DateTime dateTime) => dateTime.ToUnixTimeSeconds();
|
||||
|
||||
private class RateTick
|
||||
private struct RateTick
|
||||
{
|
||||
public double Rate { get; }
|
||||
|
||||
@@ -111,15 +128,18 @@ public partial class ProxyChartView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private async void FlushFlowChartAsync(CancellationToken token)
|
||||
private void FlushFlowChartAsync()
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var flowStatistics = IReverseProxyService.Constants.Instance.GetFlowStatistics();
|
||||
if (flowStatistics == null)
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
var isAttachedToVisualTree = this.IsAttachedToVisualTree();
|
||||
|
||||
@@ -132,7 +152,7 @@ public partial class ProxyChartView : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
var timestamp = GetTimestamp(DateTime.Now);
|
||||
var timestamp = DateTime.Now.ToUnixTimeSeconds();
|
||||
|
||||
reads.Add(new RateTick(flowStatistics.ReadRate, timestamp));
|
||||
writes.Add(new RateTick(flowStatistics.WriteRate, timestamp));
|
||||
@@ -143,24 +163,26 @@ public partial class ProxyChartView : UserControl
|
||||
this.writes.RemoveAt(0);
|
||||
}
|
||||
|
||||
if (Chart != null && isAttachedToVisualTree)
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Chart.Series = new ISeries[] { readSeries, writeSeries };
|
||||
});
|
||||
//if (Chart != null && isAttachedToVisualTree)
|
||||
//{
|
||||
// Dispatcher.UIThread.Post(() =>
|
||||
// {
|
||||
// Chart.Series = new ISeries[] { readSeries, writeSeries };
|
||||
// });
|
||||
//}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1d), CancellationToken.None);
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//解决内存泄露问题
|
||||
//https://github.com/beto-rodriguez/LiveCharts2/issues/1080#issuecomment-1601536016
|
||||
////解决内存泄露问题
|
||||
////https://github.com/beto-rodriguez/LiveCharts2/issues/1080#issuecomment-1601536016
|
||||
//private static void Chart_UpdateFinished(LiveChartsCore.Kernel.Sketches.IChartView<SkiaSharpDrawingContext> chart)
|
||||
//{
|
||||
// // Chart library leaks PaintTasks
|
||||
@@ -169,7 +191,7 @@ public partial class ProxyChartView : UserControl
|
||||
// // Periodically clean up drawables, this may cause the chart to blip if the user mouses over it during this time
|
||||
// if (chart.CoreCanvas.DrawablesCount > 50)
|
||||
// {
|
||||
// chart.CoreCanvas.SetPaintTasks(new HashSet<LiveChartsCore.Drawing.IPaint<SkiaSharpDrawingContext>>());
|
||||
// chart.CoreCanvas.SetPaintTasks([]);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
DockPanel.Dock="Right"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
|
||||
<Button
|
||||
Padding="8,4"
|
||||
Command="{Binding RefreshCommand}"
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
<MenuFlyout>
|
||||
<MenuItem
|
||||
Command="{Binding CopyCode}"
|
||||
Header="{Binding Path=Res.Up, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
Header="{Binding Path=Res.LocalAuth_Copy, Mode=OneWay, Source={x:Static s:ResourceService.Current}}"
|
||||
ToolTip.Tip="{Binding Path=Res.LocalAuth_Copy, Mode=OneWay, Source={x:Static s:ResourceService.Current}}">
|
||||
<MenuItem.Icon>
|
||||
<ui:SymbolIcon Symbol="Copy" />
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ public partial interface ISteamIdleSettings
|
||||
/// <summary>
|
||||
/// 运行规则的默认值
|
||||
/// </summary>
|
||||
static readonly IdleRule DefaultIdleRule = IdleRule.OnlyOneGame;
|
||||
static readonly IdleRule DefaultIdleRule = IdleRule.FastMode;
|
||||
|
||||
/// <summary>
|
||||
/// 运行顺序的默认值
|
||||
|
||||
@@ -223,11 +223,17 @@ public sealed partial class IdleCardPageViewModel : ViewModelBase
|
||||
|
||||
if (!SteamLoginState.Success)
|
||||
{
|
||||
var vm = new IdleSteamLoginPageViewModel(ref SteamLoginState);
|
||||
await IWindowManager.Instance.ShowTaskDialogAsync(vm, Strings.Steam_Login,
|
||||
pageContent: new IdleSteamLoginPage(), okButtonText: Strings.Confirm, isOkButton: false);
|
||||
|
||||
IsLogin = SteamLoginState.Success;
|
||||
LoginViewModel = new IdleSteamLoginPageViewModel(ref SteamLoginState);
|
||||
LoginViewModel.Close += () =>
|
||||
{
|
||||
ViewState = 1;
|
||||
IsLogin = SteamLoginState.Success;
|
||||
};
|
||||
ViewState = 0;
|
||||
//var vm = new IdleSteamLoginPageViewModel(ref SteamLoginState);
|
||||
//await IWindowManager.Instance.ShowTaskDialogAsync(vm, Strings.Steam_Login,
|
||||
// pageContent: new IdleSteamLoginPage(), okButtonText: Strings.Confirm, isOkButton: false);
|
||||
//IsLogin = SteamLoginState.Success;
|
||||
}
|
||||
|
||||
var sid = SteamConnectService.Current.CurrentSteamUser?.SteamId64;
|
||||
|
||||
@@ -68,6 +68,12 @@ public sealed partial class IdleCardPageViewModel
|
||||
[Reactive]
|
||||
public int TotalCardsRemaining { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public int ViewState { get; set; } = 1;
|
||||
|
||||
[Reactive]
|
||||
public IdleSteamLoginPageViewModel? LoginViewModel { get; set; }
|
||||
|
||||
public int DropCardsCount => Math.Max(DroppedCardsCount - TotalCardsRemaining, 0);
|
||||
|
||||
[Reactive]
|
||||
|
||||
@@ -216,16 +216,18 @@
|
||||
</StackPanel>
|
||||
</spp:PageBase.PaneContent>
|
||||
|
||||
<!--<Carousel Classes="AnimeSlide" SelectedIndex="0">-->
|
||||
<spp:ContentLoader CustomLoadingText="{Binding Path=Res.EnableAccelerationPrompt, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" IsLoading="{Binding IsLoaing}">
|
||||
<!-- IsVisible="{Binding !IsLoaing}" -->
|
||||
<Grid
|
||||
ColumnDefinitions="*,240"
|
||||
IsVisible="{Binding !IsLoaing}"
|
||||
RowDefinitions="Auto,Auto,*">
|
||||
<spp:OptionsDisplayItem Expands="True" IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Header>
|
||||
<!--<TextBlock>
|
||||
<Carousel Classes="AnimeSlide" SelectedIndex="{Binding ViewState}">
|
||||
<spp:IdleSteamLoginPage DataContext="{Binding LoginViewModel}" />
|
||||
|
||||
<spp:ContentLoader CustomLoadingText="{Binding Path=Res.EnableAccelerationPrompt, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" IsLoading="{Binding IsLoaing}">
|
||||
<!-- IsVisible="{Binding !IsLoaing}" -->
|
||||
<Grid
|
||||
ColumnDefinitions="*,240"
|
||||
IsVisible="{Binding !IsLoaing}"
|
||||
RowDefinitions="Auto,Auto,*">
|
||||
<spp:OptionsDisplayItem Expands="True" IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Header>
|
||||
<!--<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding
|
||||
@@ -243,337 +245,337 @@
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>-->
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="IsLogin" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UserName" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.Idle_NeedLoginSteam"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</spp:OptionsDisplayItem.Header>
|
||||
<spp:OptionsDisplayItem.Description>
|
||||
<Panel>
|
||||
<TextBlock IsVisible="{Binding IsLogin}">
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="IsLogin" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UserName" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_UserExpString_"
|
||||
Path="Res.Idle_NeedLoginSteam"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UserLevel" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.CurrentExp" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock IsVisible="{Binding !IsLogin}" Text="{Binding Path=Res.SteamIdle_SteamClientUserTips, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Description>
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<spp:Image2
|
||||
Width="64"
|
||||
Height="64"
|
||||
VerticalAlignment="Center"
|
||||
DecodeWidth="64"
|
||||
DockPanel.Dock="Top"
|
||||
FallbackSource="avares://BD.WTTS.Client.Avalonia/UI/Assets/avatar.jpg"
|
||||
Source="{Binding UserIdleInfo.AvatarUrl, Mode=OneWay}"
|
||||
Stretch="UniformToFill">
|
||||
<spp:Image2.Clip>
|
||||
<EllipseGeometry Rect="0,0,64,64" />
|
||||
</spp:Image2.Clip>
|
||||
</spp:Image2>
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
<spp:OptionsDisplayItem.ActionButton>
|
||||
<Button VerticalAlignment="Center" Command="{Binding LoginSteamCommand}">
|
||||
<WrapPanel>
|
||||
<ui:SymbolIcon Margin="0,0,5,0" Symbol="AddFriend" />
|
||||
<TextBlock>
|
||||
</spp:OptionsDisplayItem.Header>
|
||||
<spp:OptionsDisplayItem.Description>
|
||||
<Panel>
|
||||
<TextBlock IsVisible="{Binding IsLogin}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="IsLogin" />
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.Logout"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.User_Login"
|
||||
Path="Res.SteamIdle_UserExpString_"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UserLevel" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.CurrentExp" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</WrapPanel>
|
||||
</Button>
|
||||
</spp:OptionsDisplayItem.ActionButton>
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<StackPanel
|
||||
Margin="24,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="6">
|
||||
<TextBlock IsVisible="{Binding !IsLogin}" Text="{Binding Path=Res.SteamIdle_SteamClientUserTips, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Description>
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<spp:Image2
|
||||
Width="64"
|
||||
Height="64"
|
||||
VerticalAlignment="Center"
|
||||
DecodeWidth="64"
|
||||
DockPanel.Dock="Top"
|
||||
FallbackSource="avares://BD.WTTS.Client.Avalonia/UI/Assets/avatar.jpg"
|
||||
Source="{Binding UserIdleInfo.AvatarUrl, Mode=OneWay}"
|
||||
Stretch="UniformToFill">
|
||||
<spp:Image2.Clip>
|
||||
<EllipseGeometry Rect="0,0,64,64" />
|
||||
</spp:Image2.Clip>
|
||||
</spp:Image2>
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
<spp:OptionsDisplayItem.ActionButton>
|
||||
<Button VerticalAlignment="Center" Command="{Binding LoginSteamCommand}">
|
||||
<WrapPanel>
|
||||
<ui:SymbolIcon Margin="0,0,5,0" Symbol="AddFriend" />
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="IsLogin" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.Logout"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.User_Login"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</WrapPanel>
|
||||
</Button>
|
||||
</spp:OptionsDisplayItem.ActionButton>
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<StackPanel
|
||||
Margin="24,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="6">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontWeight="SemiBold"
|
||||
IsVisible="{Binding IsLogin}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_UserNextExpString_"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.NextLevel" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UpExp" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontWeight="SemiBold"
|
||||
IsVisible="{Binding !IsLogin}"
|
||||
Text="{Binding Source={x:Static s:ResourceService.Current}, Path=Res.Idle_NeedLoginSteam, Mode=OneWay}" />
|
||||
<ProgressBar
|
||||
Height="10"
|
||||
Margin="0,0,0,6"
|
||||
Classes="Material"
|
||||
Maximum="100"
|
||||
Value="{Binding UserIdleInfo.NextLevelExpPercentage}" />
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontWeight="SemiBold"
|
||||
IsVisible="{Binding IsLogin}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_UserNextExpString_"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.NextLevel" />
|
||||
<CompiledBinding Mode="OneWay" Path="UserIdleInfo.UpExp" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontWeight="SemiBold"
|
||||
IsVisible="{Binding !IsLogin}"
|
||||
Text="{Binding Source={x:Static s:ResourceService.Current}, Path=Res.Idle_NeedLoginSteam, Mode=OneWay}" />
|
||||
<ProgressBar
|
||||
Height="10"
|
||||
Margin="0,0,0,6"
|
||||
Classes="Material"
|
||||
Maximum="100"
|
||||
Value="{Binding UserIdleInfo.NextLevelExpPercentage}" />
|
||||
|
||||
<!--<Border
|
||||
<!--<Border
|
||||
Height="1"
|
||||
Margin="-50,6"
|
||||
Background="{DynamicResource ApplicationPageBackgroundThemeBrush}" />-->
|
||||
|
||||
<Panel>
|
||||
<WrapPanel HorizontalAlignment="Left">
|
||||
<TextBlock FontSize="36" Text="{Binding TotalCardsRemaining}" />
|
||||
<TextBlock VerticalAlignment="Center" Text=" 张剩余卡牌掉落" />
|
||||
</WrapPanel>
|
||||
<WrapPanel HorizontalAlignment="Right">
|
||||
<TextBlock VerticalAlignment="Center" Text="估算价值 ¥ " />
|
||||
<TextBlock FontSize="36" Text="{Binding TotalCardsAvgPrice, StringFormat={}{0:0.00}}" />
|
||||
</WrapPanel>
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WrapPanel HorizontalAlignment="Left">
|
||||
<TextBlock FontSize="36" Text="{Binding TotalCardsRemaining}" />
|
||||
<TextBlock VerticalAlignment="Center" Text=" 张剩余卡牌掉落" />
|
||||
</WrapPanel>
|
||||
<WrapPanel HorizontalAlignment="Right">
|
||||
<TextBlock VerticalAlignment="Center" Text="估算价值 ¥ " />
|
||||
<TextBlock FontSize="36" Text="{Binding TotalCardsAvgPrice, StringFormat={}{0:0.00}}" />
|
||||
</WrapPanel>
|
||||
</Panel>
|
||||
|
||||
<Border
|
||||
Height="1"
|
||||
Margin="-50,6"
|
||||
Background="{DynamicResource ApplicationPageBackgroundThemeBrush}" />
|
||||
<Border
|
||||
Height="1"
|
||||
Margin="-50,6"
|
||||
Background="{DynamicResource ApplicationPageBackgroundThemeBrush}" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0,6,0,0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
<TextBlock VerticalAlignment="Center" Text="当前 Steam 客户端登录账号:" />
|
||||
<spp:Image2
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
DecodeWidth="36"
|
||||
FallbackSource="avares://BD.WTTS.Client.Avalonia/UI/Assets/avatar.jpg"
|
||||
Source="{Binding Source={x:Static s:SteamConnectService.Current}, Path=CurrentSteamUser.AvatarMedium, Mode=OneWay}"
|
||||
Stretch="Uniform">
|
||||
<spp:Image2.Clip>
|
||||
<EllipseGeometry Rect="0,0,24,24" />
|
||||
</spp:Image2.Clip>
|
||||
</spp:Image2>
|
||||
<TextBlock VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="IsConnectToSteam"
|
||||
Source="{x:Static s:SteamConnectService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="CurrentSteamUser.SteamNickName"
|
||||
Source="{x:Static s:SteamConnectService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamAppType_Unknown"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<StackPanel
|
||||
Margin="0,6,0,0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
<TextBlock VerticalAlignment="Center" Text="当前 Steam 客户端登录账号:" />
|
||||
<spp:Image2
|
||||
Width="24"
|
||||
Height="24"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
DecodeWidth="36"
|
||||
FallbackSource="avares://BD.WTTS.Client.Avalonia/UI/Assets/avatar.jpg"
|
||||
Source="{Binding Source={x:Static s:SteamConnectService.Current}, Path=CurrentSteamUser.AvatarMedium, Mode=OneWay}"
|
||||
Stretch="Uniform">
|
||||
<spp:Image2.Clip>
|
||||
<EllipseGeometry Rect="0,0,24,24" />
|
||||
</spp:Image2.Clip>
|
||||
</spp:Image2>
|
||||
<TextBlock VerticalAlignment="Center">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="IsConnectToSteam"
|
||||
Source="{x:Static s:SteamConnectService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="CurrentSteamUser.SteamNickName"
|
||||
Source="{x:Static s:SteamConnectService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamAppType_Unknown"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
|
||||
<!-- Description="{Binding CurrentIdle.AppName}" -->
|
||||
<spp:OptionsDisplayItem
|
||||
Grid.Column="1"
|
||||
Expands="True"
|
||||
IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Header>
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="RunState" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_IdleRuning"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_IdleNotRuning"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</spp:OptionsDisplayItem.Header>
|
||||
<spp:OptionsDisplayItem.Description>
|
||||
<Panel>
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
IsVisible="{Binding !IsRunningSteamProcess, Source={x:Static s:SteamConnectService.Current}, Mode=OneWay}"
|
||||
Text="{Binding Path=Res.SteamNotRuning, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
|
||||
IsVisible="{Binding IsRunningSteamProcess, Source={x:Static s:SteamConnectService.Current}, Mode=OneWay}"
|
||||
Text="{Binding Path=Res.SteamRuning, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Description>
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<Panel
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="5,0,3,0">
|
||||
<Ellipse Fill="#47A42B" IsVisible="{Binding RunState}" />
|
||||
<Ellipse Fill="#FF4F58" IsVisible="{Binding !RunState}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<StackPanel
|
||||
Margin="24,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="6">
|
||||
<Panel ToolTip.Tip="{Binding DropCardsCount, Mode=OneWay, Converter={StaticResource StringFormatConverter}, ConverterParameter=SteamIdle_DroppedCardsCountTip_}">
|
||||
<Arc
|
||||
Width="80"
|
||||
Height="80"
|
||||
StartAngle="0"
|
||||
Stretch="None"
|
||||
Stroke="{DynamicResource ControlStrokeColorSecondaryBrush}"
|
||||
StrokeThickness="5"
|
||||
SweepAngle="360" />
|
||||
<ProgressBar
|
||||
Width="80"
|
||||
Height="80"
|
||||
Classes="Circle"
|
||||
Maximum="{Binding DroppedCardsCount}"
|
||||
Value="{Binding TotalCardsRemaining}" />
|
||||
<!-- Description="{Binding CurrentIdle.AppName}" -->
|
||||
<spp:OptionsDisplayItem
|
||||
Grid.Column="1"
|
||||
Expands="True"
|
||||
IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Header>
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding Converter="{StaticResource VisibleStringConverter}">
|
||||
<CompiledBinding Mode="OneWay" Path="RunState" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_IdleRuning"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
<CompiledBinding
|
||||
Mode="OneWay"
|
||||
Path="Res.SteamIdle_IdleNotRuning"
|
||||
Source="{x:Static s:ResourceService.Current}" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</spp:OptionsDisplayItem.Header>
|
||||
<spp:OptionsDisplayItem.Description>
|
||||
<Panel>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path=IdleTime, Mode=OneWay, Converter={StaticResource StringFormatConverter}}" />
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
IsVisible="{Binding !IsRunningSteamProcess, Source={x:Static s:SteamConnectService.Current}, Mode=OneWay}"
|
||||
Text="{Binding Path=Res.SteamNotRuning, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
<TextBlock
|
||||
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
|
||||
IsVisible="{Binding IsRunningSteamProcess, Source={x:Static s:SteamConnectService.Current}, Mode=OneWay}"
|
||||
Text="{Binding Path=Res.SteamRuning, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Description>
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<Panel
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="5,0,3,0">
|
||||
<Ellipse Fill="#47A42B" IsVisible="{Binding RunState}" />
|
||||
<Ellipse Fill="#FF4F58" IsVisible="{Binding !RunState}" />
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<StackPanel
|
||||
Margin="24,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="6">
|
||||
<Panel ToolTip.Tip="{Binding DropCardsCount, Mode=OneWay, Converter={StaticResource StringFormatConverter}, ConverterParameter=SteamIdle_DroppedCardsCountTip_}">
|
||||
<Arc
|
||||
Width="80"
|
||||
Height="80"
|
||||
StartAngle="0"
|
||||
Stretch="None"
|
||||
Stroke="{DynamicResource ControlStrokeColorSecondaryBrush}"
|
||||
StrokeThickness="5"
|
||||
SweepAngle="360" />
|
||||
<ProgressBar
|
||||
Width="80"
|
||||
Height="80"
|
||||
Classes="Circle"
|
||||
Maximum="{Binding DroppedCardsCount}"
|
||||
Value="{Binding TotalCardsRemaining}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Path=IdleTime, Mode=OneWay, Converter={StaticResource StringFormatConverter}}" />
|
||||
</Panel>
|
||||
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<TextBlock VerticalAlignment="Center" Text="挂卡进度 " />
|
||||
<TextBlock FontSize="22.6">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} / {1}">
|
||||
<CompiledBinding Path="DropCardsCount" />
|
||||
<CompiledBinding Path="DroppedCardsCount" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</WrapPanel>
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<TextBlock VerticalAlignment="Center" Text="挂卡进度 " />
|
||||
<TextBlock FontSize="22.6">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} / {1}">
|
||||
<CompiledBinding Path="DropCardsCount" />
|
||||
<CompiledBinding Path="DroppedCardsCount" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</WrapPanel>
|
||||
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<TextBlock FontSize="24.8" Text="{Binding RuningCount, Mode=OneWay}" />
|
||||
<TextBlock VerticalAlignment="Center" Text=" 个游戏正在运行" />
|
||||
</WrapPanel>
|
||||
<WrapPanel HorizontalAlignment="Center">
|
||||
<TextBlock FontSize="24.8" Text="{Binding RuningCount, Mode=OneWay}" />
|
||||
<TextBlock VerticalAlignment="Center" Text=" 个游戏正在运行" />
|
||||
</WrapPanel>
|
||||
|
||||
<!--<TextBlock
|
||||
<!--<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
TextAlignment="Center">
|
||||
<Run FontSize="22.6" Text="{Binding Badges.Count}" />
|
||||
<Run Text=" 个徽章" />
|
||||
</TextBlock>-->
|
||||
</StackPanel>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
|
||||
<spp:OptionsDisplayItem
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Description="{Binding IdleGameList.Count, Converter={StaticResource StringFormatConverter}, ConverterParameter=SteamIdle_RemainingGames_}"
|
||||
Expands="True"
|
||||
Header="挂卡游戏列表"
|
||||
IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<ui:ImageIcon Source="{Binding Icon, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource BitmapAssetValueConverter}}" />
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
|
||||
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<Panel>
|
||||
<StackPanel
|
||||
Margin="0,30"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
IsVisible="{Binding !IdleGameList.Count}"
|
||||
Text="未获取到挂卡游戏数据" />
|
||||
</StackPanel>
|
||||
<ItemsRepeater DockPanel.Dock="Bottom" ItemsSource="{Binding IdleGameList}">
|
||||
<ItemsRepeater.Layout>
|
||||
<UniformGridLayout
|
||||
ItemsJustification="Start"
|
||||
ItemsStretch="Uniform"
|
||||
MinColumnSpacing="5"
|
||||
MinItemHeight="160"
|
||||
MinItemWidth="280"
|
||||
MinRowSpacing="5" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate DataType="spp:IdleApp">
|
||||
<spp:AppItem
|
||||
Title="{Binding App.DisplayName}"
|
||||
ContextFlyout="{StaticResource SharedMenuFlyout}"
|
||||
Description="{Binding Tags}"
|
||||
MoreFlyout="{StaticResource SharedMenuFlyout}"
|
||||
Tags="{Binding AppId}">
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<Panel>
|
||||
<WrapPanel IsVisible="{Binding App.Process, Converter={StaticResource IsNullConverter}, ConverterParameter=invert, Mode=OneWay}">
|
||||
<Ellipse
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="0,0,5,0"
|
||||
Fill="#47A42B" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{Binding Path=Res.GameList_RunState, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</WrapPanel>
|
||||
<WrapPanel IsVisible="{Binding App.Process, Converter={StaticResource IsNullConverter}, Mode=OneWay}">
|
||||
<Ellipse
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="0,0,5,0"
|
||||
Fill="#FF4F58" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{Binding Path=Res.GameList_StopState, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</WrapPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<!--<spp:AppItem.ActionButton>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
|
||||
<spp:OptionsDisplayItem
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Description="{Binding IdleGameList.Count, Converter={StaticResource StringFormatConverter}, ConverterParameter=SteamIdle_RemainingGames_}"
|
||||
Expands="True"
|
||||
Header="挂卡游戏列表"
|
||||
IsExpanded="True">
|
||||
<spp:OptionsDisplayItem.Icon>
|
||||
<ui:ImageIcon Source="{Binding Icon, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource BitmapAssetValueConverter}}" />
|
||||
</spp:OptionsDisplayItem.Icon>
|
||||
|
||||
|
||||
<spp:OptionsDisplayItem.Content>
|
||||
<Panel>
|
||||
<StackPanel
|
||||
Margin="0,30"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
IsVisible="{Binding !IdleGameList.Count}"
|
||||
Text="未获取到挂卡游戏数据" />
|
||||
</StackPanel>
|
||||
<ItemsRepeater DockPanel.Dock="Bottom" ItemsSource="{Binding IdleGameList}">
|
||||
<ItemsRepeater.Layout>
|
||||
<UniformGridLayout
|
||||
ItemsJustification="Start"
|
||||
ItemsStretch="Uniform"
|
||||
MinColumnSpacing="5"
|
||||
MinItemHeight="160"
|
||||
MinItemWidth="280"
|
||||
MinRowSpacing="5" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate DataType="spp:IdleApp">
|
||||
<spp:AppItem
|
||||
Title="{Binding App.DisplayName}"
|
||||
ContextFlyout="{StaticResource SharedMenuFlyout}"
|
||||
Description="{Binding Tags}"
|
||||
MoreFlyout="{StaticResource SharedMenuFlyout}"
|
||||
Tags="{Binding AppId}">
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<Panel>
|
||||
<WrapPanel IsVisible="{Binding App.Process, Converter={StaticResource IsNullConverter}, ConverterParameter=invert, Mode=OneWay}">
|
||||
<Ellipse
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="0,0,5,0"
|
||||
Fill="#47A42B" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{Binding Path=Res.GameList_RunState, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</WrapPanel>
|
||||
<WrapPanel IsVisible="{Binding App.Process, Converter={StaticResource IsNullConverter}, Mode=OneWay}">
|
||||
<Ellipse
|
||||
Width="6"
|
||||
Height="6"
|
||||
Margin="0,0,5,0"
|
||||
Fill="#FF4F58" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{Binding Path=Res.GameList_StopState, Mode=OneWay, Source={x:Static s:ResourceService.Current}}" />
|
||||
</WrapPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<!--<spp:AppItem.ActionButton>
|
||||
<Button
|
||||
Command="{Binding $parent[spp:PageBase].((spp:IdleAppsPageViewModel)DataContext).DeleteButtonCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
@@ -586,9 +588,9 @@
|
||||
</Viewbox>
|
||||
</Button>
|
||||
</spp:AppItem.ActionButton>-->
|
||||
<spp:AppItem.Image>
|
||||
<Panel x:Name="ItemImage">
|
||||
<!--<i:Interaction.Behaviors>
|
||||
<spp:AppItem.Image>
|
||||
<Panel x:Name="ItemImage">
|
||||
<!--<i:Interaction.Behaviors>
|
||||
<ia:EventTriggerBehavior EventName="PointerEntered" SourceObject="{Binding #ItemImage}">
|
||||
<ia:ChangePropertyAction
|
||||
PropertyName="IsVisible"
|
||||
@@ -602,50 +604,50 @@
|
||||
Value="False" />
|
||||
</ia:EventTriggerBehavior>
|
||||
</i:Interaction.Behaviors>-->
|
||||
<Panel MinWidth="{Binding $parent[spp:AppItem].MinWidth}" MaxWidth="{Binding $parent[spp:AppItem].MaxWidth}">
|
||||
<spp:Image2
|
||||
Name="AppImage"
|
||||
DecodeWidth="120"
|
||||
FallbackSource="avares://BD.WTTS.Client.Plugins.GameList/UI/Assets/defaultappimage.png"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding App.LibraryGridStream^}"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
IsVisible="{Binding #AppImage.IsFailed}"
|
||||
Text="{Binding App.DisplayName}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<Panel MinWidth="{Binding $parent[spp:AppItem].MinWidth}" MaxWidth="{Binding $parent[spp:AppItem].MaxWidth}">
|
||||
<spp:Image2
|
||||
Name="AppImage"
|
||||
DecodeWidth="120"
|
||||
FallbackSource="avares://BD.WTTS.Client.Plugins.GameList/UI/Assets/defaultappimage.png"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
Source="{Binding App.LibraryGridStream^}"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock
|
||||
Margin="8,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
IsVisible="{Binding #AppImage.IsFailed}"
|
||||
Text="{Binding App.DisplayName}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Theme="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
|
||||
<Border
|
||||
Margin="10,0,10,-3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
CornerRadius="3"
|
||||
IsVisible="{Binding App.IsInstalled}">
|
||||
<DockPanel Margin="10,3" HorizontalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
IsVisible="{Binding App.InstalledDrive, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}"
|
||||
Text="{Binding App.InstalledDrive}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding App.SizeOnDisk, Converter={StaticResource StringFormatConverter}, ConverterParameter=size}"
|
||||
TextWrapping="Wrap" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
<!--<Panel
|
||||
<Border
|
||||
Margin="10,0,10,-3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
CornerRadius="3"
|
||||
IsVisible="{Binding App.IsInstalled}">
|
||||
<DockPanel Margin="10,3" HorizontalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
IsVisible="{Binding App.InstalledDrive, Converter={StaticResource IsNullConverter}, ConverterParameter=invert}"
|
||||
Text="{Binding App.InstalledDrive}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||
Text="{Binding App.SizeOnDisk, Converter={StaticResource StringFormatConverter}, ConverterParameter=size}"
|
||||
TextWrapping="Wrap" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
<!--<Panel
|
||||
x:Name="ItemBox"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
IsVisible="False"
|
||||
@@ -692,18 +694,16 @@
|
||||
</Viewbox>
|
||||
</Button>
|
||||
</Panel>-->
|
||||
</Panel>
|
||||
</spp:AppItem.Image>
|
||||
</spp:AppItem>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
</Grid>
|
||||
</spp:ContentLoader>
|
||||
|
||||
<!--<spp:IdleSteamLoginPage />-->
|
||||
<!--</Carousel>-->
|
||||
</Panel>
|
||||
</spp:AppItem.Image>
|
||||
</spp:AppItem>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Panel>
|
||||
</spp:OptionsDisplayItem.Content>
|
||||
</spp:OptionsDisplayItem>
|
||||
</Grid>
|
||||
</spp:ContentLoader>
|
||||
</Carousel>
|
||||
</spp:PageBase>
|
||||
|
||||
@@ -154,6 +154,7 @@ public static class ThirdPartyLoginHelper
|
||||
AppResources.User_Register);
|
||||
close?.Invoke();
|
||||
Toast.Show(ToastIcon.Success, msg);
|
||||
await UserService.Current.RefreshShopTokenAsync();
|
||||
}
|
||||
|
||||
static IDisposable? serverDisposable;
|
||||
|
||||
@@ -65,10 +65,24 @@ public static class Migrations
|
||||
{
|
||||
MoveAccelerator();
|
||||
}
|
||||
|
||||
//var v_3_0_rc4 = new Version(3, 0, 407);
|
||||
//if (IApplication.IsDesktop() && PreviousVersion < v_3_0_rc4)
|
||||
//{
|
||||
// ClearScriptFile();
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ClearScriptFile()
|
||||
{
|
||||
var scriptDirectory = Path.Combine(IOPath.AppDataDirectory, AssemblyInfo.Plugins, AssemblyInfo.Accelerator, DirName_Scripts);
|
||||
Directory.Delete(scriptDirectory, true);
|
||||
var scriptCacheDirectory = Path.Combine(IOPath.CacheDirectory, AssemblyInfo.Plugins, AssemblyInfo.Accelerator, DirName_BuildScripts);
|
||||
Directory.Delete(scriptCacheDirectory, true);
|
||||
}
|
||||
|
||||
static void MoveDirByScripts(string rootPath, string? newDirName = null)
|
||||
{
|
||||
try
|
||||
@@ -77,11 +91,14 @@ public static class Migrations
|
||||
AssemblyInfo.Plugins,
|
||||
AssemblyInfo.Accelerator,
|
||||
newDirName ?? DirName_Scripts);
|
||||
if (!Directory.Exists(newPath))
|
||||
var dir = new DirectoryInfo(newPath);
|
||||
if (!dir.Exists)
|
||||
{
|
||||
var oldPath = Path.Combine(rootPath, DirName_Scripts);
|
||||
if (Directory.Exists(oldPath))
|
||||
{
|
||||
//创建至父级目录
|
||||
dir.Parent?.Create();
|
||||
Directory.Move(oldPath, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,16 @@ sealed class MicroServiceClient : MicroServiceClientBase
|
||||
}
|
||||
}
|
||||
|
||||
public sealed override async Task SaveShopAuthTokenAsync(JWTEntity authToken)
|
||||
{
|
||||
var user = await userManager.GetCurrentUserAsync();
|
||||
if (user != null)
|
||||
{
|
||||
user.ShopAuthToken = authToken;
|
||||
await userManager.SetCurrentUserAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed override async Task OnLoginedAsync(
|
||||
IReadOnlyPhoneNumber? phoneNumber,
|
||||
ILoginResponse response)
|
||||
|
||||
@@ -199,6 +199,39 @@ public sealed class UserService : ReactiveObject
|
||||
await RefreshUserAsync(user, refreshCurrentUser);
|
||||
}
|
||||
|
||||
public async Task RefreshShopTokenAsync(bool refreshToken = true)
|
||||
{
|
||||
var currentUser = await userManager.GetCurrentUserAsync();
|
||||
|
||||
if (refreshToken && currentUser != null)
|
||||
{
|
||||
var token = await csc.Shop.GetShopUserTokenAsync();
|
||||
if (token.IsSuccess)
|
||||
{
|
||||
currentUser.ShopAuthToken = token.Content;
|
||||
await userManager.SetCurrentUserAsync(currentUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带登录状态跳转 Url
|
||||
/// </summary>
|
||||
/// <param name="url">跳转地址</param>
|
||||
/// <returns></returns>
|
||||
public async Task OpenAuthUrl(string url)
|
||||
{
|
||||
var token = await userManager.GetAuthTokenAsync();
|
||||
if (token == null)
|
||||
{
|
||||
Browser2.Open(url.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Browser2.Open(string.Format(Constants.Urls.OfficialWebsite_Fast_Login_, token.AccessToken, HttpUtility.UrlEncode(token.ExpiresIn.ToString()), HttpUtility.UrlEncode(url)));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshUserAvatarAsync()
|
||||
{
|
||||
if (User != null)
|
||||
|
||||
@@ -86,6 +86,9 @@ public sealed partial class DebugPageViewModel : TabItemViewModel
|
||||
break;
|
||||
case "updatetray":
|
||||
|
||||
break;
|
||||
case "gc":
|
||||
GC.Collect();
|
||||
break;
|
||||
case "login":
|
||||
if (cmds.Length > 1)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using BD.WTTS.Client.Resources;
|
||||
using DynamicData;
|
||||
using static BD.WTTS.Services.IMicroServiceClient;
|
||||
|
||||
namespace BD.WTTS.UI.ViewModels;
|
||||
@@ -21,7 +22,8 @@ public sealed class HomePageViewModel : TabItemViewModel
|
||||
public ObservableCollection<AdvertisementDTO> NavigationBanners { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public ObservableCollection<List<ShopRecommendGoodItem>> Shops { get; set; }
|
||||
//public ObservableCollection<List<ShopRecommendGoodItem>> Shops { get; set; }
|
||||
public ObservableCollection<ShopRecommendGoodItem> Shops { get; set; }
|
||||
|
||||
public ICommand NavgationToMenuPageCommand { get; }
|
||||
|
||||
@@ -31,7 +33,8 @@ public sealed class HomePageViewModel : TabItemViewModel
|
||||
Articles = new ObservableCollection<ArticleItemDTO>() { null, null, null };
|
||||
#pragma warning restore CS8625 // 无法将 null 字面量转换为非 null 的引用类型。
|
||||
NavigationBanners = new ObservableCollection<AdvertisementDTO>();
|
||||
Shops = new ObservableCollection<List<ShopRecommendGoodItem>>();
|
||||
//Shops = new ObservableCollection<List<ShopRecommendGoodItem>>();
|
||||
Shops = new ObservableCollection<ShopRecommendGoodItem>();
|
||||
|
||||
NavgationToMenuPageCommand = ReactiveCommand.Create<TabItemViewModel>(NavgationToMenuPage);
|
||||
|
||||
@@ -55,12 +58,12 @@ public sealed class HomePageViewModel : TabItemViewModel
|
||||
var result2 = await Instance.Shop.RecommendGoods();
|
||||
if (result2.IsSuccess && result2.Content != null)
|
||||
{
|
||||
Shops.Clear();
|
||||
var data = result2.Content
|
||||
.Select((v, i) => new { v, i })
|
||||
.GroupBy(x => x.i / 4)
|
||||
.Select(g => g.Select(x => x.v).ToList());
|
||||
Shops.Add(data);
|
||||
//Shops.Clear();
|
||||
//var data = result2.Content
|
||||
// .Select((v, i) => new { v, i })
|
||||
// .GroupBy(x => x.i / 4)
|
||||
// .Select(g => g.Select(x => x.v).ToList());
|
||||
Shops.Add(result2.Content);
|
||||
}
|
||||
|
||||
var result3 = await Instance.Advertisement.All(AdvertisementType.DeskTopHomeBanner);
|
||||
|
||||
@@ -45,6 +45,7 @@ public partial class LoginOrRegisterWindowViewModel : WindowViewModel, SendSmsUI
|
||||
var msg = Strings.Success_.Format((rsp?.IsLoginOrRegister ?? false) ? Strings.User_Login : Strings.User_Register);
|
||||
close?.Invoke();
|
||||
Toast.Show(ToastIcon.Success, msg);
|
||||
await UserService.Current.RefreshShopTokenAsync();
|
||||
}
|
||||
|
||||
async Task SendSmsAsync() => await this.SendSmsAsync(() => new()
|
||||
|
||||
Reference in New Issue
Block a user