初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<Grid x:Class="PCL.MyMsgLogin"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
|
||||
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="TransformRotate" Angle="-4" />
|
||||
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<controls:BlurBorder Name="PanBorder" CornerRadius="7"
|
||||
Background="{DynamicResource ColorBrushBackground}"
|
||||
MouseLeftButtonDown="Drag">
|
||||
<controls:BlurBorder.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="4"
|
||||
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
|
||||
</controls:BlurBorder.Effect>
|
||||
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2" />
|
||||
<RowDefinition Height="13" />
|
||||
<RowDefinition Height="1*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
|
||||
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9"
|
||||
Text="{DynamicResource Launch.Account.LoginDialog.Title}"
|
||||
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False"
|
||||
MouseLeftButtonDown="Drag" />
|
||||
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
|
||||
<local:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17"
|
||||
Padding="7,0,15,0"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
DeltaMult="0.7">
|
||||
<TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabCaption"
|
||||
FontSize="15" LineHeight="18"
|
||||
Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" Padding="1" />
|
||||
</local:MyScrollViewer>
|
||||
<StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
|
||||
Margin="150,0,8,0" Orientation="Horizontal">
|
||||
<local:MyButton ColorType="Normal" x:FieldModifier="public"
|
||||
Text="{DynamicResource Launch.Account.LoginDialog.ReopenWebpage}" x:Name="Btn1"
|
||||
Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
|
||||
UseLayoutRounding="False"
|
||||
local:CustomEventService.EventType="OpenUrl" />
|
||||
<local:MyButton ColorType="Normal" x:FieldModifier="public"
|
||||
Text="{DynamicResource Launch.Account.LoginDialog.CopyCode}" x:Name="Btn2"
|
||||
Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
|
||||
UseLayoutRounding="False"
|
||||
local:CustomEventService.EventType="CopyText" />
|
||||
<local:MyButton ColorType="Normal" x:FieldModifier="public"
|
||||
Text="{DynamicResource Common.Action.Cancel}" x:Name="Btn3" Margin="12,0,0,0"
|
||||
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
|
||||
Click="Btn3_Click" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</controls:BlurBorder>
|
||||
</Grid>
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI.Controls;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MyMsgLogin
|
||||
{
|
||||
private readonly JsonObject data;
|
||||
private string deviceCode; // 用于轮询的设备代码
|
||||
private string oAuthUrl = ""; // OAuth 轮询验证地址
|
||||
private string userCode; // 需要用户在网页上输入的设备代码
|
||||
private string website; // 验证网页的网址
|
||||
private Task? workingThread;
|
||||
|
||||
public MyMsgLogin()
|
||||
{
|
||||
InitializeComponent();
|
||||
// Handles
|
||||
Loaded += Load;
|
||||
Btn1.Click += Btn1_Click;
|
||||
Btn3.Click += Btn3_Click;
|
||||
PanBorder.MouseLeftButtonDown += Drag;
|
||||
LabTitle.MouseLeftButtonDown += Drag;
|
||||
}
|
||||
|
||||
private void Finished(object result)
|
||||
{
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
myConverter.IsExited = true;
|
||||
myConverter.Result = result;
|
||||
ModBase.RunInUi(Close);
|
||||
Thread.Sleep(200);
|
||||
ModMain.frmMain.ShowWindowToTop();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
userCode = (string)data["user_code"];
|
||||
deviceCode = (string)data["device_code"];
|
||||
ModBase.ClipboardSet(deviceCode);
|
||||
if (data["verification_uri_complete"] is not null)
|
||||
{
|
||||
website = (string)data["verification_uri_complete"];
|
||||
LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", userCode, website);
|
||||
}
|
||||
else
|
||||
{
|
||||
website = (string)data["verification_uri"];
|
||||
LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", userCode, website);
|
||||
}
|
||||
|
||||
// 设置 UI
|
||||
LabTitle.Text = Lang.Text("Launch.Account.LoginDialog.MinecraftLogin");
|
||||
CustomEventService.SetEventData(Btn1, website);
|
||||
CustomEventService.SetEventData(Btn2, userCode);
|
||||
// 启动工作线程
|
||||
workingThread = WorkThreadAsync();
|
||||
}
|
||||
|
||||
private record ErrorBody(
|
||||
[property: JsonPropertyName("error")] string Error,
|
||||
[property: JsonPropertyName("error_description")] string Desc);
|
||||
|
||||
private async Task WorkThreadAsync()
|
||||
{
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
if (myConverter.IsExited)
|
||||
return;
|
||||
ModBase.OpenWebsite(website);
|
||||
ModBase.ClipboardSet(userCode);
|
||||
var delayTime = (data["interval"].ToObject<int>() - 1) * 1000;
|
||||
// 轮询
|
||||
var unknownFailureCount = 0;
|
||||
while (!myConverter.IsExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={deviceCode}&scope=XboxLive.signin%20offline_access";
|
||||
using var result = await HttpRequest
|
||||
.Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
|
||||
.WithFormContent(bodyData)
|
||||
.SendAsync(enableLogging: false)
|
||||
.ConfigureAwait(false);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
var error = await result.AsJsonAsync<ErrorBody>()
|
||||
.ConfigureAwait(false);
|
||||
switch(error?.Error)
|
||||
{
|
||||
case "authorization_pending":
|
||||
{
|
||||
await Task.Delay(delayTime)
|
||||
.ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new Exception(error?.Error ?? "Unable to get body");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取结果
|
||||
var ctx = await result.AsStringAsync().ConfigureAwait(false);
|
||||
var resultJson = (JsonObject)ModBase.GetJson(ctx);
|
||||
ModProfile.ProfileLog($"令牌过期时间:{resultJson["expires_in"]} 秒");
|
||||
HintService.Hint(Lang.Text("Launch.Account.LoginDialog.Success"), HintType.Success);
|
||||
Finished(new[] { resultJson["access_token"].ToString(), resultJson["refresh_token"].ToString() });
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (unknownFailureCount <= 2)
|
||||
{
|
||||
unknownFailureCount += 1;
|
||||
ModBase.Log(ex, $"正版验证轮询第 {unknownFailureCount} 次失败");
|
||||
ModBase.Log(ex.Message);
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Finished(new Exception(Lang.Text("Launch.Account.LoginDialog.PollingFailed"), ex));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region 弹窗
|
||||
|
||||
private readonly ModMain.MyMsgBoxConverter myConverter;
|
||||
private readonly int uuid = ModBase.GetUuid();
|
||||
|
||||
public MyMsgLogin(ModMain.MyMsgBoxConverter converter)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeComponent();
|
||||
Btn1.Name += ModBase.GetUuid();
|
||||
Btn2.Name += ModBase.GetUuid();
|
||||
Btn3.Name += ModBase.GetUuid();
|
||||
myConverter = converter;
|
||||
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
|
||||
data = (JsonObject)converter.Content;
|
||||
oAuthUrl = converter.AuthUrl?.ToString() ?? "";
|
||||
Init();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.LoginDialog.Error.Init"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Account.LoginDialog.Error.Init"));
|
||||
}
|
||||
|
||||
Loaded += Load;
|
||||
}
|
||||
|
||||
private void Load(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 动画
|
||||
Opacity = 0d;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
|
||||
(myConverter.IsWarn
|
||||
? new ModBase.MyColor(140d, 80d, 0d, 0d)
|
||||
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
|
||||
"PanMsgBackground Background");
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(this, 1d, 120, 60),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
|
||||
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
-TransformRotate.Angle, 300, 60,
|
||||
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
|
||||
}, "MyMsgBox " + uuid);
|
||||
// 记录日志
|
||||
ModBase.Log($"[Control] 正版验证弹窗:{LabTitle.Text}\r\n{LabCaption.Text}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.LoginDialog.Error.Load"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Account.LoginDialog.Error.Load"));
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
// 动画
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!ModMain.WaitingMyMsgBox.Any())
|
||||
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
|
||||
BlurBorder.BackgroundProperty,
|
||||
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
|
||||
}, 30),
|
||||
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
|
||||
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
|
||||
150, 0, new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
|
||||
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
|
||||
}, "MyMsgBox " + uuid);
|
||||
}
|
||||
|
||||
// 实现回车和 Esc 的接口(#4857)
|
||||
public void Btn1_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
public void Btn3_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Finished(new ThreadInterruptedException());
|
||||
}
|
||||
|
||||
private void Drag(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// On Error Resume Next
|
||||
if (e.GetPosition(ShapeLine).Y <= 2d)
|
||||
ModMain.frmMain.DragMove();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Grid x:Class="PCL.MySkin"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
Height="64" Width="64" UseLayoutRounding="True" RenderTransformOrigin="0.5,0.5"
|
||||
Background="{StaticResource ColorBrushSemiTransparent}"
|
||||
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="-50" ToolTipService.HorizontalOffset="2"
|
||||
ToolTipService.InitialShowDelay="100">
|
||||
<Grid.Effect>
|
||||
<DropShadowEffect x:Name="ShadowSkin" BlurRadius="10" ShadowDepth="0" Color="{DynamicResource ColorObject2}"
|
||||
Opacity="0.2" />
|
||||
</Grid.Effect>
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<Grid.ContextMenu>
|
||||
<ContextMenu>
|
||||
<local:MyMenuItem x:Name="BtnSkinSave" Header="{DynamicResource Launch.Skin.SaveAs}" Padding="0,2"
|
||||
Checked="BtnSkinSave_Checked"
|
||||
Icon="F1 M 48,39L 56,39L 56,49L 63.25,49L 52,60.25L 40.75,49L 48,49L 48,39 Z M 20,20L 50.25,20L 56,25.75L 56,38L 52,38L 52,27.25L 48.75,24L 48,24L 48,37L 28,37L 28,24L 24,24L 24,52L 42.25,52L 46.25,56L 20,56L 20,20 Z M 39,24L 39,34L 44,34L 44,24L 39,24 Z" />
|
||||
<local:MyMenuItem x:Name="BtnSkinRefresh" Header="{DynamicResource Common.Action.Refresh}" Padding="0,2"
|
||||
Icon="F1 M 38,20.5833C 42.9908,20.5833 47.4912,22.6825 50.6667,26.046L 50.6667,17.4167L 55.4166,22.1667L 55.4167,34.8333L 42.75,34.8333L 38,30.0833L 46.8512,30.0833C 44.6768,27.6539 41.517,26.125 38,26.125C 31.9785,26.125 27.0037,30.6068 26.2296,36.4167L 20.6543,36.4167C 21.4543,27.5397 28.9148,20.5833 38,20.5833 Z M 38,49.875C 44.0215,49.875 48.9963,45.3932 49.7703,39.5833L 55.3457,39.5833C 54.5457,48.4603 47.0852,55.4167 38,55.4167C 33.0092,55.4167 28.5088,53.3175 25.3333,49.954L 25.3333,58.5833L 20.5833,53.8333L 20.5833,41.1667L 33.25,41.1667L 38,45.9167L 29.1487,45.9167C 31.3231,48.3461 34.483,49.875 38,49.875 Z" />
|
||||
<local:MyMenuItem x:Name="BtnSkinCape" Header="{DynamicResource Launch.Skin.ChangeCape}" Padding="0,2"
|
||||
Icon="M764.330761 922.841896h-496.758545c-18.741345 0-33.869901-15.128556-33.869901-33.8699V540.789416l-68.642999 29.805512c-15.805954 6.77398-34.0957 0.903197-42.676075-13.999559L22.354135 386.116869c-7.225579-12.418964-5.870783-28.224917 3.38699-39.063286 36.805292-42.901874 256.959647-176.349283 300.764718-202.767806 15.805954-9.483572 36.353693-4.515987 46.063065 11.064168 43.579272 69.320397 86.932745 74.739581 143.156781 74.739581s99.577508-5.419184 143.156781-74.739581c9.709372-15.580154 30.257111-20.54774 46.063065-11.064168 43.805072 26.418523 263.959427 159.865932 300.764719 202.767806 9.483572 11.064168 10.838368 26.644322 3.38699 39.063286l-100.029107 170.4785c-8.580375 14.676957-26.870121 20.773539-42.676075 13.999559l-68.642999-29.805512v348.18258c0.451599 18.741345-14.676957 33.869901-33.418302 33.8699z m-462.888644-67.739801h429.018743v-365.794929c0-11.515766 5.644983-22.128335 15.354355-28.224917 9.483572-6.322381 21.676736-7.225579 32.063506-2.709592l88.287541 38.385888 70.900993-120.802646c-44.25667-34.547299-153.091951-104.093495-239.798898-157.15634-57.578831 72.481588-123.286439 79.029768-181.316869 79.029768s-123.738037-6.548181-181.316868-79.029768c-86.706946 52.837045-195.768026 122.609041-239.798898 157.15634L165.510915 496.758545l88.513341-38.611687c10.38677-4.515987 22.579934-3.612789 32.063506 2.709592 9.483572 6.322381 15.354355 16.93495 15.354355 28.224917v366.020728z" />
|
||||
</ContextMenu>
|
||||
</Grid.ContextMenu>
|
||||
<Image x:Name="ImgBack" Width="48" Height="48" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor"
|
||||
SnapsToDevicePixels="True" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
<Image x:Name="ImgFore" Width="56" Height="56" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor"
|
||||
SnapsToDevicePixels="True" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
@@ -0,0 +1,498 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class MySkin
|
||||
{
|
||||
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
|
||||
|
||||
// 皮肤储存
|
||||
private bool isChanging;
|
||||
|
||||
// 点击
|
||||
private bool isSkinMouseDown;
|
||||
public ModLoader.LoaderTask<ModBase.EqualableList<string>, string> loader;
|
||||
|
||||
public MySkin()
|
||||
{
|
||||
InitializeComponent();
|
||||
MouseEnter += PanSkin_MouseEnter;
|
||||
MouseLeave += PanSkin_MouseLeave;
|
||||
MouseLeftButtonDown += PanSkin_MouseLeftButtonDown;
|
||||
MouseLeftButtonUp += PanSkin_MouseLeftButtonUp;
|
||||
// Handles
|
||||
BtnSkinSave.Click += BtnSkinSave_Click;
|
||||
BtnSkinSave.Checked += BtnSkinSave_Checked;
|
||||
BtnSkinRefresh.Click += RefreshClick;
|
||||
BtnSkinCape.Click += BtnSkinCape_Click;
|
||||
}
|
||||
|
||||
public string Address
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ToolTip = string.IsNullOrEmpty(field)
|
||||
? Lang.Text("Common.State.Loading")
|
||||
: Lang.Text("Launch.Skin.Change.ToolTip");
|
||||
}
|
||||
}
|
||||
|
||||
// 披风
|
||||
public bool HasCape
|
||||
{
|
||||
get => BtnSkinCape.Visibility == Visibility.Collapsed;
|
||||
set => BtnSkinCape.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 事件
|
||||
public event ClickEventHandler? Click;
|
||||
|
||||
// 控件动画
|
||||
private void PanSkin_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.8d - ShadowSkin.Opacity, 200, 100), "Skin Shadow");
|
||||
}
|
||||
|
||||
private void PanSkin_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.2d - ShadowSkin.Opacity, 200), "Skin Shadow");
|
||||
isSkinMouseDown = false;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60,
|
||||
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
|
||||
}
|
||||
|
||||
private void PanSkin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
isSkinMouseDown = true;
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(this, 0.9d - ((ScaleTransform)RenderTransform).ScaleX, 60,
|
||||
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
|
||||
}
|
||||
|
||||
private void PanSkin_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
ModAnimation.AniStart(
|
||||
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60,
|
||||
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
|
||||
if (!isSkinMouseDown) return;
|
||||
isSkinMouseDown = false;
|
||||
Click?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
// 保存皮肤
|
||||
public void BtnSkinSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Save(loader);
|
||||
}
|
||||
|
||||
public static void Save(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> loader)
|
||||
{
|
||||
var address = loader.output;
|
||||
if (loader.State != ModBase.LoadState.Finished)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Fetching"), HintType.Error);
|
||||
if (loader.State != ModBase.LoadState.Loading)
|
||||
loader.Start();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileAddress = SystemDialogs.SelectSaveFile(Lang.Text("Launch.Skin.SaveDialog.Title"),
|
||||
ModBase.GetFileNameFromPath(address),
|
||||
Lang.Text("Launch.Skin.SaveDialog.Filter"));
|
||||
if (!fileAddress.Contains(@"\")) return;
|
||||
File.Delete(fileAddress);
|
||||
if (address.StartsWith(ModBase.pathImage))
|
||||
{
|
||||
var image = new MyBitmap(address);
|
||||
image.Save(fileAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.CopyFile(address, fileAddress);
|
||||
}
|
||||
|
||||
HintService.Hint(Lang.Text("Launch.Skin.SaveSuccess"), HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Save.Error"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Save.Error"));
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnSkinSave_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
((MyMenuItem)sender).IsEnabled = string.IsNullOrEmpty(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 载入皮肤。
|
||||
/// </summary>
|
||||
public void Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查文件存在
|
||||
Address = loader.output;
|
||||
if (string.IsNullOrEmpty(Address))
|
||||
throw new Exception("皮肤加载器 " + loader.name + " 没有输出");
|
||||
if (!Address.StartsWith(ModBase.pathImage) && !File.Exists(Address))
|
||||
throw new FileNotFoundException("皮肤文件未找到", Address);
|
||||
// 加载
|
||||
MyBitmap image;
|
||||
try
|
||||
{
|
||||
image = new MyBitmap(Address);
|
||||
}
|
||||
catch (Exception ex) // #2272
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Load.Error.Corrupted", Address),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Load.Error.Corrupted", Address));
|
||||
File.Delete(Address);
|
||||
return;
|
||||
}
|
||||
|
||||
ImgBack.Tag = Address;
|
||||
// 大小检查
|
||||
var scale = (int)Math.Round(image.pic.Width / 64d);
|
||||
if (image.pic.Width < 32 || image.pic.Height < 32)
|
||||
{
|
||||
ImgFore.Source = null;
|
||||
ImgBack.Source = null;
|
||||
throw new Exception("图片大小不足,长为 " + image.pic.Height + ",宽为 " + image.pic.Width);
|
||||
}
|
||||
|
||||
MyBitmap skinHead = null;
|
||||
// 头发层(附加层)
|
||||
if (image.pic.Width >= 64 && image.pic.Height >= 32)
|
||||
{
|
||||
if (image.pic.GetPixel(1, 1).A == 0 ||
|
||||
image.pic.GetPixel(image.pic.Width - 1, image.pic.Height - 1).A == 0 ||
|
||||
image.pic.GetPixel(image.pic.Width - 2, (int)Math.Round(image.pic.Height / 2d - 2d)).A == 0 ||
|
||||
(image.pic.GetPixel(1, 1) != image.pic.GetPixel(scale * 41, scale * 9) &&
|
||||
image.pic.GetPixel(image.pic.Width - 1, image.pic.Height - 1) !=
|
||||
image.pic.GetPixel(scale * 41, scale * 9) &&
|
||||
image.pic.GetPixel(image.pic.Width - 2, (int)Math.Round(image.pic.Height / 2d - 2d)) !=
|
||||
image.pic.GetPixel(scale * 41, scale * 9))) // 如果图片中有任何透明像素(避免纯色白底)
|
||||
// 或是头部颜色和透明区均不一样
|
||||
{
|
||||
ImgFore.Source = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
|
||||
skinHead = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImgFore.Source = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImgFore.Source = null;
|
||||
}
|
||||
|
||||
// 脸层
|
||||
ImgBack.Source = image.Clip(scale * 8, scale * 8, scale * 8, scale * 8);
|
||||
// 用于显示档案列表头像的图片
|
||||
var skinHeadId = Address.Between(new[] { Address.Contains("Images/Skins/") ? "Skins/" : @"Skin\" }[0],
|
||||
".png");
|
||||
var cachePath = ModBase.pathTemp + $@"Cache\Skin\Head\{skinHeadId}.png";
|
||||
ModProfile.selectedProfile.SkinHeadId = skinHeadId;
|
||||
ModProfile.SaveProfile();
|
||||
var completeHead = new Bitmap(56, 56);
|
||||
using (var g = Graphics.FromImage(completeHead))
|
||||
{
|
||||
g.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||||
g.PixelOffsetMode = PixelOffsetMode.Half;
|
||||
using (Bitmap faceBitmap = image.Clip(scale * 8, scale * 8, scale * 8, scale * 8))
|
||||
{
|
||||
g.DrawImage(faceBitmap, new Rectangle(4, 4, 48, 48));
|
||||
}
|
||||
|
||||
if (ImgFore.Source is not null)
|
||||
{
|
||||
using Bitmap hairBitmap = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
|
||||
g.DrawImage(hairBitmap, new Rectangle(0, 0, 56, 56));
|
||||
}
|
||||
}
|
||||
|
||||
if (!Directory.Exists(ModBase.pathTemp + @"Cache\Skin\Head"))
|
||||
Directory.CreateDirectory(ModBase.pathTemp + @"Cache\Skin\Head");
|
||||
completeHead.Save(cachePath, ImageFormat.Png);
|
||||
ModBase.Log("[Skin] 载入头像成功:" + loader.name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Load.Error.Avatar", $"{(Address ?? "null")},{loader.name}"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Load.Error.Avatar", $"{(Address ?? "null")},{loader.name}"));
|
||||
}
|
||||
}
|
||||
|
||||
private object ScaleToSize(Bitmap bitmap, int width, int height)
|
||||
{
|
||||
var scaledBitmap = new Bitmap(width, height);
|
||||
using var g = Graphics.FromImage(scaledBitmap);
|
||||
g.InterpolationMode = InterpolationMode.NearestNeighbor;
|
||||
g.PixelOffsetMode = PixelOffsetMode.Half;
|
||||
g.DrawImage(bitmap, 0, 0, width, height);
|
||||
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空皮肤。
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Address = "";
|
||||
ImgFore.Source = null;
|
||||
ImgBack.Source = null;
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
public void RefreshClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RefreshCache(loader);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新皮肤缓存。
|
||||
/// </summary>
|
||||
public static void RefreshCache(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> sender = null)
|
||||
{
|
||||
var hasLoaderRunning =
|
||||
PageLaunchLeft.skinLoaders.Any(skinLoader => skinLoader.State == ModBase.LoadState.Loading);
|
||||
|
||||
if (ModMain.frmLaunchLeft is not null && hasLoaderRunning)
|
||||
// 由于 Abort 不是实时的,暂时不会释放文件,会导致删除报错,故只能取消执行
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Refresh.Busy"));
|
||||
else
|
||||
// 清空缓存
|
||||
// 刷新控件
|
||||
ModBase.RunInThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Refreshing"));
|
||||
ModBase.Log("[Skin] 正在清空皮肤缓存");
|
||||
if (Directory.Exists(ModBase.pathTemp + @"Cache\Skin"))
|
||||
ModBase.DeleteDirectory(ModBase.pathTemp + @"Cache\Skin");
|
||||
if (Directory.Exists(ModBase.pathTemp + @"Cache\Uuid"))
|
||||
ModBase.DeleteDirectory(ModBase.pathTemp + @"Cache\Uuid");
|
||||
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Skin\IndexMs.ini");
|
||||
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Skin\IndexAuth.ini");
|
||||
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Uuid\Mojang.ini");
|
||||
foreach (var SkinLoader in sender is not null
|
||||
? new[] { sender }
|
||||
: new[] { PageLaunchLeft.skinLegacy, PageLaunchLeft.skinMs })
|
||||
SkinLoader.WaitForExit(isForceRestart: true);
|
||||
HintService.Hint(Lang.Text("Launch.Skin.RefreshSuccess"), HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Refresh.Error"),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Skin.Refresh.Error"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在更换正版皮肤后,刷新正版皮肤。
|
||||
/// </summary>
|
||||
/// <param name="skinAddress">新的正版皮肤完整地址。</param>
|
||||
public static void ReloadCache(string skinAddress)
|
||||
{
|
||||
// 更新缓存
|
||||
// 刷新控件
|
||||
// 完成提示
|
||||
ModBase.RunInThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ModBase.WriteIni(ModBase.pathTemp + @"Cache\Skin\IndexMs.ini", ModProfile.selectedProfile.Uuid,
|
||||
skinAddress);
|
||||
ModBase.Log($"[Skin] 已写入皮肤地址缓存 {ModProfile.selectedProfile.Uuid} -> {skinAddress}");
|
||||
PageLaunchLeft.skinMs.WaitForExit(isForceRestart: true);
|
||||
HintService.Hint(Lang.Text("Launch.Skin.ChangeSuccess"), HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Change.Error.MsRefresh"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Launch.Skin.Change.Error.MsRefresh"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void BtnSkinCape_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 检查条件,获取新披风
|
||||
if (isChanging)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Cape.Changing"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Failed)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Cape.LoginFailed"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Cape.FetchingList"));
|
||||
isChanging = true;
|
||||
// 开始实际获取
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取登录信息
|
||||
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
|
||||
ModLaunch.mcLoginMsLoader.WaitForExit(ModProfile.GetLoginData());
|
||||
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Cape.LoginFailed"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var accessToken = ModLaunch.mcLoginMsLoader.output.AccessToken;
|
||||
var uuid = ModLaunch.mcLoginMsLoader.output.Uuid;
|
||||
var skinData = (JsonObject)ModBase.GetJson(ModLaunch.mcLoginMsLoader.output.ProfileJson);
|
||||
foreach (var itemSkin in skinData["capes"].AsArray())
|
||||
{
|
||||
if (itemSkin["url"] is null)
|
||||
continue;
|
||||
var localFile = $@"{ModBase.pathTemp}Cache\Capes\{itemSkin["alias"]}.png";
|
||||
var capeFrontFile = $@"{ModBase.pathTemp}Cache\Capes\{itemSkin["alias"]}-front.png";
|
||||
if (File.Exists(localFile) && File.Exists(capeFrontFile))
|
||||
{
|
||||
itemSkin["url"] = capeFrontFile;
|
||||
continue;
|
||||
}
|
||||
|
||||
FileDownloader.DownloadByLoader(itemSkin["url"].ToString(), localFile);
|
||||
var capeFrontRegion = new Rectangle(1, 0, 11, 17);
|
||||
var capeFront = new Bitmap(capeFrontRegion.Width, capeFrontRegion.Height);
|
||||
var capeImage = Image.FromFile(localFile);
|
||||
var gra = Graphics.FromImage(capeFront);
|
||||
gra.DrawImage(capeImage, capeFrontRegion, capeFrontRegion, GraphicsUnit.Pixel);
|
||||
capeFront.Save(capeFrontFile);
|
||||
itemSkin["url"] = capeFrontFile;
|
||||
}
|
||||
|
||||
// 获取玩家的所有披风
|
||||
int? selId = null;
|
||||
ModBase.RunInUiWait(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var selectionControl = new List<IMyRadio>
|
||||
{
|
||||
new MyListItem
|
||||
{
|
||||
Title = Lang.Text("Launch.Skin.Cape.None"),
|
||||
Info = "Null"
|
||||
}
|
||||
};
|
||||
selectionControl.AddRange(from Cape in skinData["capes"].AsArray()
|
||||
let CapeAlias = Cape["alias"].ToString()
|
||||
let CapeName = _GetCapeDisplayName(CapeAlias)
|
||||
let state = Cape["state"]
|
||||
let active = state is not null && state.ToString().ToUpper().Equals("ACTIVE")
|
||||
select new MyListItem
|
||||
{
|
||||
Title = CapeName,
|
||||
Info = Cape["alias"].ToString(),
|
||||
Checked = active,
|
||||
Type = MyListItem.CheckType.RadioBox,
|
||||
Logo = (string)Cape["url"],
|
||||
LogoScale = 0.8d
|
||||
});
|
||||
|
||||
selId = ModMain.MyMsgBoxSelect(selectionControl, Lang.Text("Launch.Skin.Cape.SelectTitle"),
|
||||
Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Cape.Error.List"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Launch.Skin.Cape.Error.List"));
|
||||
}
|
||||
});
|
||||
if (selId is null)
|
||||
return;
|
||||
// 发送请求
|
||||
var result = Requester.Fetch("https://api.minecraftservices.com/minecraft/profile/capes/active",
|
||||
new FetchParam
|
||||
{
|
||||
Method = selId is 0 ? "DELETE" : "PUT",
|
||||
Content = selId is 0
|
||||
? ""
|
||||
: new JsonObject { ["capeId"] = skinData["capes"][(int)(selId - 1)]["id"]?.ToString() }.ToJsonString(),
|
||||
ContentType = "application/json",
|
||||
Headers = new Dictionary<string, string> { { "Authorization", "Bearer " + accessToken } }
|
||||
}
|
||||
);
|
||||
if (result.Contains("\"errorMessage\""))
|
||||
HintService.Hint(
|
||||
Lang.Text("Launch.Skin.Cape.ChangeFailedWithReason",
|
||||
((JsonObject)ModBase.GetJson(result))["errorMessage"]), HintType.Error);
|
||||
else
|
||||
HintService.Hint(Lang.Text("Launch.Skin.Cape.ChangeSuccess"), HintType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Cape.ChangeFailed"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Cape.ChangeFailed"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
isChanging = false;
|
||||
}
|
||||
}, "Cape Change");
|
||||
}
|
||||
|
||||
private static string _GetCapeDisplayName(string capeAlias)
|
||||
{
|
||||
var safeName = capeAlias
|
||||
.Replace("-", "")
|
||||
.Replace(" ", "")
|
||||
.Replace("'", "");
|
||||
var key = $"Launch.Skin.Cape.Name.{safeName}";
|
||||
var name = Lang.Text(key);
|
||||
if (name == $"!{key}!" || name == key)
|
||||
return capeAlias;
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<local:MyPageLeft x:Class="PCL.PageLaunchLeft"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d" x:Name="PanBack"
|
||||
d:DesignHeight="417.2" Width="300">
|
||||
<Grid Name="PanInput" RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="20" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="10" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform />
|
||||
</Grid.RenderTransform>
|
||||
<local:MyButton Grid.Row="3" Grid.Column="2" x:Name="BtnInstance" Height="35" Margin="0,10,10,0" Text="{DynamicResource Launch.Home.SelectInstance}"
|
||||
IsEnabled="False" Click="BtnInstance_Click" />
|
||||
<local:MyButton Grid.Row="3" Grid.Column="3" x:Name="BtnMore" Visibility="Collapsed" Height="35"
|
||||
Click="BtnMore_Click"
|
||||
Margin="0,10,10,0" TextPadding="36" Text="{DynamicResource Launch.Home.InstanceSettings}" />
|
||||
<Grid Grid.ColumnSpan="5" Grid.Row="1" Margin="20,0" VerticalAlignment="Center" x:Name="PanLogin" />
|
||||
<Grid Grid.ColumnSpan="5" Grid.Row="2" RenderTransformOrigin="0.5,0.5">
|
||||
<Grid.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform x:Name="AprilScaleTrans" />
|
||||
<TranslateTransform x:Name="AprilPosTrans" />
|
||||
</TransformGroup>
|
||||
</Grid.RenderTransform>
|
||||
<local:MyButton x:Name="BtnLaunch" Height="54" Margin="20,0" Text="{DynamicResource Launch.Home.Button.Loading}" ColorType="Highlight"
|
||||
Click="BtnLaunch_Click"
|
||||
Padding="30,0,30,15" IsEnabled="False" />
|
||||
<TextBlock x:Name="LabVersion" Text="{DynamicResource Launch.Home.VersionList.Loading}" Margin="35,0,35,10" IsHitTestVisible="False"
|
||||
VerticalAlignment="Bottom" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||
FontSize="11" Foreground="{DynamicResource ColorBrushGray3}" RenderTransformOrigin="0.5,-0.2">
|
||||
<TextBlock.RenderTransform>
|
||||
<ScaleTransform ScaleX="{Binding RealRenderTransform.ScaleX, ElementName=BtnLaunch, Mode=OneWay}"
|
||||
ScaleY="{Binding RealRenderTransform.ScaleY, ElementName=BtnLaunch, Mode=OneWay}" />
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid Name="PanLaunching" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed" Opacity="0">
|
||||
<Grid.RenderTransform>
|
||||
<ScaleTransform ScaleX="0.8" ScaleY="0.8" />
|
||||
</Grid.RenderTransform>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Row="1" RenderTransformOrigin="0.5,0.5" Margin="0,-7,0,0">
|
||||
<StackPanel.BitmapEffect>
|
||||
<DropShadowBitmapEffect Color="{StaticResource ColorObjectGray2}" ShadowDepth="1.5" Direction="270"
|
||||
Opacity="0.15" Softness="0.15" />
|
||||
</StackPanel.BitmapEffect>
|
||||
<local:MyLoading x:Name="LoadLaunching" AutoRun="False" Height="50" Margin="0,10,0,5" />
|
||||
<TextBlock Name="LabLaunchingTitle" Margin="15,10,15,0" Text="{DynamicResource Launch.Status.Title.Launching}" HorizontalAlignment="Center"
|
||||
FontSize="20" Foreground="{DynamicResource ColorBrush3}">
|
||||
<TextBlock.RenderTransform>
|
||||
<SkewTransform AngleX="-3" />
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
<TextBlock Name="LabLaunchingName" Margin="40,5,40,0" FontSize="13.5" Text="Forge 1.12.2-15.8.0.1560"
|
||||
HorizontalAlignment="Center" Foreground="{DynamicResource ColorBrush3}"
|
||||
RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<SkewTransform AngleX="-3" />
|
||||
</TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
<Grid Height="4" Margin="30,12,30,27" SnapsToDevicePixels="True">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Name="ProgressLaunchingFinished" Width="69.28*" />
|
||||
<ColumnDefinition Name="ProgressLaunchingUnfinished" Width="30.72*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle Grid.Column="0">
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
|
||||
<GradientStop Color="{DynamicResource ColorObject4}" Offset="0" />
|
||||
<GradientStop Color="{DynamicResource ColorObject3}" Offset="0.6" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Rectangle Grid.Column="1" Fill="{DynamicResource ColorBrush6}" Opacity="0.6" />
|
||||
</Grid>
|
||||
<Grid HorizontalAlignment="Center" Name="PanLaunchingInfo" SizeChanged="PanLaunchingInfo_SizeChangedW">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="15" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.CurrentStep}" Grid.Row="0" Grid.Column="1"
|
||||
HorizontalAlignment="Right" Opacity="0.5" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.DownloadLibs}" Grid.Row="0" Grid.Column="3"
|
||||
HorizontalAlignment="Left" Name="LabLaunchingStage" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.LoginMethod}" Grid.Row="1" Grid.Column="1"
|
||||
HorizontalAlignment="Right" Opacity="0.5" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.MicrosoftLogin}" Grid.Row="1" Grid.Column="3"
|
||||
HorizontalAlignment="Left" MaxWidth="160" TextWrapping="WrapWithOverflow"
|
||||
Name="LabLaunchingMethod" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.LaunchProgress}" Grid.Row="2" Grid.Column="1"
|
||||
HorizontalAlignment="Right" Opacity="0.5" Name="LabLaunchingProgressLeft" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="69.28 %" Grid.Row="2" Grid.Column="3"
|
||||
HorizontalAlignment="Left" Name="LabLaunchingProgress" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.DownloadSpeed}" Grid.Row="3" Grid.Column="1"
|
||||
HorizontalAlignment="Right" Opacity="0" Name="LabLaunchingDownloadLeft"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="5.2 M/s" Grid.Row="3" Grid.Column="3"
|
||||
HorizontalAlignment="Left" Name="LabLaunchingDownload" Opacity="0" Visibility="Collapsed" />
|
||||
<Grid x:Name="PanLaunchingHint" HorizontalAlignment="Center" Grid.Row="2" Grid.ColumnSpan="5"
|
||||
Grid.RowSpan="2" Width="260" Visibility="Collapsed" Margin="0,16,0,2" Opacity="0">
|
||||
<Border Margin="0,8,0,0" BorderThickness="1" CornerRadius="3"
|
||||
BorderBrush="{DynamicResource ColorBrushGray1}" Opacity="0.5">
|
||||
<Border.Clip>
|
||||
<CombinedGeometry GeometryCombineMode="Exclude">
|
||||
<CombinedGeometry.Geometry1>
|
||||
<RectangleGeometry Rect="0,0,1000,1000" />
|
||||
</CombinedGeometry.Geometry1>
|
||||
<CombinedGeometry.Geometry2>
|
||||
<RectangleGeometry Rect="94,0,72,10" />
|
||||
</CombinedGeometry.Geometry2>
|
||||
</CombinedGeometry>
|
||||
</Border.Clip>
|
||||
</Border>
|
||||
<TextBlock FontSize="12.5" Text="{DynamicResource Launch.Status.Trivia}" HorizontalAlignment="Center" VerticalAlignment="Top"
|
||||
Opacity="0.5" />
|
||||
<TextBlock x:Name="LabLaunchingHint" FontSize="12.5" Text="{DynamicResource Launch.Status.Trivia.Placeholder}"
|
||||
Margin="11,21,11,10" TextWrapping="Wrap" HorizontalAlignment="Center" MaxHeight="54"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<local:MyButton Grid.Row="4" x:Name="BtnCancel" Height="35" Margin="20,0,20,20" VerticalAlignment="Bottom"
|
||||
Click="BtnCancel_Click"
|
||||
Text="{DynamicResource Common.Action.Cancel}" />
|
||||
</Grid>
|
||||
</local:MyPageLeft>
|
||||
@@ -0,0 +1,1033 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Network;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLaunchLeft
|
||||
{
|
||||
private double actualUsedHeight;
|
||||
private double actualUsedWidth;
|
||||
private int btnLaunchState;
|
||||
private string _btnLaunchLanguage;
|
||||
private McInstance btnLaunchVersion;
|
||||
private bool isHeightAnimating;
|
||||
public interface ILoginPage { void Reload(); }
|
||||
|
||||
private enum LaunchButtonAction
|
||||
{
|
||||
Loading,
|
||||
Launch,
|
||||
Download,
|
||||
Disabled
|
||||
}
|
||||
|
||||
private LaunchButtonAction _launchButtonAction;
|
||||
|
||||
private static string StageWaitWindow => Lang.Text("Minecraft.Launch.Stage.WaitWindow");
|
||||
private static string StageEnd => Lang.Text("Minecraft.Launch.Stage.End");
|
||||
private static string StageRoot => Lang.Text("Minecraft.Launch.Stage.Root");
|
||||
|
||||
// 加载当前实例
|
||||
private bool isLoad;
|
||||
|
||||
private bool isLoadFinished;
|
||||
|
||||
// 尺寸改变动画
|
||||
private bool isWidthAnimating;
|
||||
private double showProgress;
|
||||
|
||||
public PageLaunchLeft()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += PageLaunchLeft_Loaded;
|
||||
WeakLanguageChanged.Add(this, OnLanguageChanged);
|
||||
// Handles
|
||||
BtnInstance.Click += BtnInstance_Click;
|
||||
BtnLaunch.Click += BtnLaunch_Click;
|
||||
BtnLaunch.Loaded += (_, _) => RefreshButtonsUI();
|
||||
BtnCancel.Click += BtnCancel_Click;
|
||||
BtnMore.Click += BtnMore_Click;
|
||||
PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedW;
|
||||
PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedH;
|
||||
}
|
||||
private static void OnLanguageChanged(PageLaunchLeft page) => ModBase.RunInUi(page.RefreshButtonsUI);
|
||||
|
||||
public void PageLaunchLeft_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (isLoad)
|
||||
RefreshPage(false);
|
||||
|
||||
AprilPosTrans.X = 0d;
|
||||
AprilPosTrans.Y = 0d;
|
||||
|
||||
if (isLoad)
|
||||
return;
|
||||
isLoad = true;
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
|
||||
// 开始按钮
|
||||
ModInstanceList.mcInstanceListLoader.LoadingStateChanged += (_, _) => RefreshButtonsUI();
|
||||
ModFolder.mcFolderListLoader.LoadingStateChanged += (_, _) => RefreshButtonsUI();
|
||||
RefreshButtonsUI();
|
||||
|
||||
// 初始化档案
|
||||
ModProfile.GetProfile();
|
||||
if (!(ModProfile.profileList.Count == 0) && ModProfile.lastUsedProfile >= 0 &&
|
||||
ModProfile.lastUsedProfile < ModProfile.profileList.Count)
|
||||
ModProfile.selectedProfile = ModProfile.profileList[ModProfile.lastUsedProfile];
|
||||
|
||||
// 加载实例
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
// 自动整合包安装:准备
|
||||
string packInstallPath = null;
|
||||
if (File.Exists(Path.Combine(ModBase.exePath, "modpack.zip")))
|
||||
packInstallPath = Path.Combine(ModBase.exePath, "modpack.zip");
|
||||
if (File.Exists(Path.Combine(ModBase.exePath, "modpack.mrpack")))
|
||||
packInstallPath = Path.Combine(ModBase.exePath, "modpack.mrpack");
|
||||
if (packInstallPath is not null)
|
||||
{
|
||||
ModBase.Log("[Launch] 需自动安装整合包:" + packInstallPath, ModBase.LogLevel.Debug);
|
||||
States.Game.SelectedFolder = @"$.minecraft\";
|
||||
if (!Directory.Exists(ModBase.exePath + @".minecraft\"))
|
||||
{
|
||||
Directory.CreateDirectory(ModBase.exePath + @".minecraft\");
|
||||
Directory.CreateDirectory(ModBase.exePath + @".minecraft\versions\");
|
||||
ModFolder.McFolderLauncherProfilesJsonCreate(ModBase.exePath + @".minecraft\");
|
||||
}
|
||||
|
||||
PageSelectLeft.AddFolder(ModBase.exePath + @".minecraft\",
|
||||
ModBase.GetFolderNameFromPath(ModBase.exePath), false);
|
||||
ModFolder.mcFolderListLoader.WaitForExit();
|
||||
}
|
||||
|
||||
// 确认 Minecraft 文件夹存在
|
||||
ModFolder.mcFolderSelected =
|
||||
States.Game.SelectedFolder.ToString().Replace("$", ModBase.exePath);
|
||||
if (string.IsNullOrEmpty(ModFolder.mcFolderSelected) || !Directory.Exists(ModFolder.mcFolderSelected))
|
||||
{
|
||||
// 无效的文件夹
|
||||
if (string.IsNullOrEmpty(ModFolder.mcFolderSelected))
|
||||
ModBase.Log("[Launch] 没有已储存的 Minecraft 文件夹");
|
||||
else
|
||||
ModBase.Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" + ModFolder.mcFolderSelected,
|
||||
ModBase.LogLevel.Debug);
|
||||
ModFolder.mcFolderListLoader.WaitForExit(isForceRestart: true);
|
||||
States.Game.SelectedFolder = ModFolder.mcFolderList[0].Location.Replace(ModBase.exePath, "$");
|
||||
}
|
||||
|
||||
ModBase.Log("[Launch] Minecraft 文件夹:" + ModFolder.mcFolderSelected);
|
||||
if (Config.Debug.AddRandomDelay)
|
||||
Thread.Sleep(RandomUtils.NextInt(500, 3000));
|
||||
// 自动整合包安装
|
||||
if (packInstallPath is not null)
|
||||
try
|
||||
{
|
||||
var installLoader = ModModpack.ModpackInstall(packInstallPath);
|
||||
ModBase.Log("[Launch] 自动安装整合包已开始:" + packInstallPath);
|
||||
installLoader.WaitForExit();
|
||||
if (installLoader.State == ModBase.LoadState.Finished)
|
||||
{
|
||||
ModBase.Log("[Launch] 自动安装整合包成功,清理安装包:" + packInstallPath);
|
||||
if (File.Exists(packInstallPath))
|
||||
File.Delete(packInstallPath);
|
||||
}
|
||||
}
|
||||
catch (ModBase.CancelledException ex)
|
||||
{
|
||||
ModBase.Log(ex, "自动安装整合包被用户取消:" + packInstallPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Select.Folder.Error.InstallPack", packInstallPath),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Select.Folder.Error.InstallPack", packInstallPath));
|
||||
}
|
||||
|
||||
// 确认 Minecraft 版本实例
|
||||
var selection = States.Game.SelectedInstance;
|
||||
var instance = selection == "" ? null : new McInstance(selection);
|
||||
if (instance is null || !instance.PathInstance.StartsWithF(ModFolder.mcFolderSelected) ||
|
||||
!instance.Check())
|
||||
{
|
||||
// 无效的实例
|
||||
ModBase.Log("[Launch] 当前选择的 Minecraft 实例无效:" + (instance is null ? "null" : instance.PathInstance),
|
||||
instance is null ? ModBase.LogLevel.Normal : ModBase.LogLevel.Debug);
|
||||
if (ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished)
|
||||
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
|
||||
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true);
|
||||
if (ModInstanceList.mcInstanceList.Count == 0 ||
|
||||
ModInstanceList.mcInstanceList.First().Value[0].Logo.Contains("RedstoneBlock"))
|
||||
{
|
||||
instance = null;
|
||||
States.Game.SelectedInstance = "";
|
||||
ModBase.Log("[Launch] 无可用 Minecraft 实例");
|
||||
}
|
||||
else
|
||||
{
|
||||
instance = ModInstanceList.mcInstanceList.First().Value[0];
|
||||
States.Game.SelectedInstance = instance.Name;
|
||||
ModBase.Log("[Launch] 自动选择 Minecraft 实例:" + instance.PathInstance);
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
ModInstanceList.McMcInstanceSelected = instance; // 绕这一圈是为了避免 McInstanceCheck 触发第二次实例改变
|
||||
isLoadFinished = true;
|
||||
RefreshButtonsUI();
|
||||
RefreshPage(false); // 有可能选择的版本变化了,需要重新刷新
|
||||
// If IsProfileVaild() = "" Then McLoginLoader.Start() '自动登录
|
||||
});
|
||||
}, "Instance Check", ThreadPriority.AboveNormal);
|
||||
|
||||
// 改变页面
|
||||
RefreshPage(false);
|
||||
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
}
|
||||
|
||||
// 实例选择按钮
|
||||
private void BtnInstance_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading)
|
||||
return;
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect);
|
||||
}
|
||||
|
||||
// 启动按钮
|
||||
public void LaunchButtonClick()
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading || !BtnLaunch.IsEnabled ||
|
||||
(ModMain.frmMain.pageRight is not null &&
|
||||
ModMain.frmMain.pageRight.PageState != MyPageRight.PageStates.ContentStay &&
|
||||
ModMain.frmMain.pageRight.PageState != MyPageRight.PageStates.ContentEnter))
|
||||
return;
|
||||
// 愚人节处理
|
||||
if (ModMain.isAprilEnabled && !ModMain.isAprilGiveup)
|
||||
{
|
||||
ModMain.isAprilGiveup = true;
|
||||
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleX = 1d;
|
||||
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleY = 1d;
|
||||
ModMain.frmLaunchLeft.AprilPosTrans.X = 0d;
|
||||
ModMain.frmLaunchLeft.AprilPosTrans.Y = 0d;
|
||||
ModMain.frmMain.BtnExtraApril.ShowRefresh();
|
||||
}
|
||||
|
||||
// 实际的启动
|
||||
switch (_launchButtonAction)
|
||||
{
|
||||
case LaunchButtonAction.Launch:
|
||||
{
|
||||
if (File.Exists(ModInstanceList.McMcInstanceSelected.PathInstance + ".pclignore"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Home.Instance.InstallingCannotLaunch"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModLaunch.McLaunchStart();
|
||||
break;
|
||||
}
|
||||
case LaunchButtonAction.Download:
|
||||
{
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshButtonsUI()
|
||||
{
|
||||
if (!BtnLaunch.IsLoaded)
|
||||
return;
|
||||
// 获取当前状态
|
||||
int currentState;
|
||||
if (!isLoadFinished || ModInstanceList.mcInstanceListLoader.State == ModBase.LoadState.Loading ||
|
||||
ModFolder.mcFolderListLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
currentState = 0;
|
||||
}
|
||||
else if (ModInstanceList.McMcInstanceSelected is null)
|
||||
{
|
||||
if (Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow)
|
||||
currentState = 1;
|
||||
else
|
||||
currentState = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentState = 3;
|
||||
}
|
||||
|
||||
// 更新状态。
|
||||
var currentLanguage = LocalizationService.CurrentLanguage.Code;
|
||||
if (currentState == btnLaunchState &&
|
||||
currentLanguage == _btnLaunchLanguage &&
|
||||
((ModInstanceList.McMcInstanceSelected is null ? "" : ModInstanceList.McMcInstanceSelected.PathInstance) ?? "") ==
|
||||
((btnLaunchVersion is null ? "" : btnLaunchVersion.PathInstance) ?? ""))
|
||||
goto ExitRefresh;
|
||||
_btnLaunchLanguage = currentLanguage;
|
||||
btnLaunchVersion = ModInstanceList.McMcInstanceSelected;
|
||||
btnLaunchState = currentState;
|
||||
switch (currentState)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
_launchButtonAction = LaunchButtonAction.Loading;
|
||||
ModBase.Log("[Minecraft] 启动按钮:正在加载 Minecraft 实例");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Loading");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
|
||||
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.Loading");
|
||||
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = false;
|
||||
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
_launchButtonAction = LaunchButtonAction.Disabled;
|
||||
ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,下载已禁用");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Launch");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
|
||||
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.NotFound");
|
||||
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
|
||||
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
_launchButtonAction = LaunchButtonAction.Download;
|
||||
ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,要求下载");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Download");
|
||||
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = true;
|
||||
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.NotFound");
|
||||
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
|
||||
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
_launchButtonAction = LaunchButtonAction.Launch;
|
||||
ModBase.Log("[Minecraft] 启动按钮:Minecraft 实例:" + ModInstanceList.McMcInstanceSelected.PathInstance);
|
||||
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Launch");
|
||||
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
|
||||
if (ModProfile.selectedProfile is not null)
|
||||
BtnLaunch.IsEnabled = true;
|
||||
else
|
||||
BtnLaunch.IsEnabled = false;
|
||||
ModMain.frmLaunchLeft.LabVersion.Text = ModInstanceList.McMcInstanceSelected.Name;
|
||||
break;
|
||||
}
|
||||
// FrmLaunchLeft.BtnMore.Visibility = Visibility.Visible '由功能隐藏设置修改
|
||||
}
|
||||
|
||||
ExitRefresh: ;
|
||||
|
||||
// 功能隐藏
|
||||
ModMain.frmLaunchLeft.BtnInstance.Visibility =
|
||||
!PageSetupUI.HiddenForceShow && Config.Preference.Hide.FunctionSelect
|
||||
? Visibility.Collapsed
|
||||
: Visibility.Visible;
|
||||
if (currentState == 3) ModMain.frmLaunchLeft.BtnMore.Visibility = ModMain.frmLaunchLeft.BtnInstance.Visibility;
|
||||
}
|
||||
|
||||
// 取消按钮
|
||||
private void BtnCancel_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoaderReal is not null)
|
||||
{
|
||||
ModLaunch.mcLaunchLoaderReal.Abort();
|
||||
ModLaunch.McLaunchLog("已取消启动");
|
||||
try
|
||||
{
|
||||
if (ModLaunch.mcLaunchWatcher is not null)
|
||||
ModLaunch.mcLaunchWatcher.Kill();
|
||||
else if (ModLaunch.mcLaunchProcess is not null)
|
||||
if (!ModLaunch.mcLaunchProcess.HasExited)
|
||||
ModLaunch.mcLaunchProcess.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.CancelProcess"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.CancelProcess"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例设置按钮
|
||||
private void BtnMore_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading)
|
||||
return;
|
||||
ModInstanceList.McMcInstanceSelected.Load();
|
||||
PageInstanceLeft.McInstance = ModInstanceList.McMcInstanceSelected;
|
||||
if (File.Exists(ModInstanceList.McMcInstanceSelected.PathInstance + ".pclignore"))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Home.Instance.InstallingCannotSetup"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSetup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每 0.2s 执行一次,刷新启动的数据 UI 显示。
|
||||
/// </summary>
|
||||
public void LaunchingRefresh()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ModLaunch.mcLaunchLoaderReal.State == ModBase.LoadState.Aborted)
|
||||
return;
|
||||
// 阶段状态获取
|
||||
var isLaunched = false; // 是否已经启动游戏,只是在等待窗口
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
var exitTry = false;
|
||||
foreach (var Loader in ModLaunch.mcLaunchLoaderReal.GetLoaderList(false))
|
||||
if (Loader.State == ModBase.LoadState.Loading || Loader.State == ModBase.LoadState.Waiting)
|
||||
{
|
||||
LabLaunchingStage.Text = Loader.name;
|
||||
isLaunched = Loader.name == StageWaitWindow || Loader.name == StageEnd;
|
||||
exitTry = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (exitTry) break;
|
||||
LabLaunchingStage.Text = Lang.Text("Launch.Status.Completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取是否启动完成失败,可能是由于启动状态改变导致集合已修改");
|
||||
return;
|
||||
}
|
||||
} while (false);
|
||||
|
||||
if (ModAnimation.AniIsRun("Launch State Page"))
|
||||
isLaunched = false; // 等待页面切换动画完成
|
||||
// 计算应显示的进度
|
||||
var actualProgress = ModLaunch.mcLaunchLoaderReal.Progress;
|
||||
if (actualProgress >= showProgress)
|
||||
showProgress += (actualProgress - showProgress) * 0.2d + 0.005d; // 向实际进度靠一点
|
||||
if (actualProgress <= showProgress)
|
||||
showProgress = actualProgress; // 原来或处理后变得比实际进度高,直接回退
|
||||
if (isLaunched)
|
||||
showProgress = 1d; // 如果已经完成了,就不卖关子了
|
||||
// 文本
|
||||
LabLaunchingTitle.Text = isLaunched ? Lang.Text("Launch.Status.Title.Launched") :
|
||||
ModLaunch.currentLaunchOptions.SaveBatch is null ? Lang.Text("Launch.Status.Title.Launching") : Lang.Text("Launch.Status.Title.ExportingScript");
|
||||
LabLaunchingProgress.Text = Lang.Number(showProgress, "P2");
|
||||
var hasLaunchDownloader = false;
|
||||
try
|
||||
{
|
||||
foreach (var Loader in ModNet.NetManager.Tasks)
|
||||
if (Loader.RealParent is not null && Loader.RealParent.name == StageRoot &&
|
||||
Loader.State == ModBase.LoadState.Loading)
|
||||
hasLaunchDownloader = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "获取 Minecraft 启动下载器失败,可能是因为启动被取消");
|
||||
hasLaunchDownloader = false;
|
||||
}
|
||||
|
||||
LabLaunchingDownload.Text = ModBase.GetString(ModNet.NetManager.Speed) + "/s";
|
||||
var shouldShowHint = Config.Preference.ShowLaunchingHint;
|
||||
// 进度改变动画
|
||||
var animList = new List<ModAnimation.AniData>
|
||||
{
|
||||
ModAnimation.AaGridLengthWidth(ProgressLaunchingFinished,
|
||||
showProgress - ProgressLaunchingFinished.Width.Value, 260,
|
||||
ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaGridLengthWidth(ProgressLaunchingUnfinished,
|
||||
1d - showProgress - ProgressLaunchingUnfinished.Width.Value, 260,
|
||||
ease: new ModAnimation.AniEaseOutFluent())
|
||||
};
|
||||
var isDownloadStateChanged =
|
||||
hasLaunchDownloader == (LabLaunchingDownload.Visibility == Visibility.Collapsed);
|
||||
if (isDownloadStateChanged)
|
||||
{
|
||||
LabLaunchingDownload.Visibility = Visibility.Visible;
|
||||
LabLaunchingDownloadLeft.Visibility = Visibility.Visible;
|
||||
animList.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(LabLaunchingDownload,
|
||||
(hasLaunchDownloader ? 1 : 0) - LabLaunchingDownload.Opacity, 100),
|
||||
ModAnimation.AaOpacity(LabLaunchingDownloadLeft,
|
||||
(hasLaunchDownloader ? 0.5d : 0d) - LabLaunchingDownloadLeft.Opacity, 100),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
if (!hasLaunchDownloader)
|
||||
{
|
||||
LabLaunchingDownload.Visibility = Visibility.Collapsed;
|
||||
LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}, 110)
|
||||
});
|
||||
}
|
||||
|
||||
var isProgressStateChanged = !isLaunched == (LabLaunchingProgress.Visibility == Visibility.Collapsed);
|
||||
if (isProgressStateChanged)
|
||||
{
|
||||
LabLaunchingProgress.Visibility = Visibility.Visible;
|
||||
LabLaunchingProgressLeft.Visibility = Visibility.Visible;
|
||||
if (isLaunched && shouldShowHint) PanLaunchingHint.Visibility = Visibility.Visible;
|
||||
animList.AddRange(new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(LabLaunchingProgress, (!isLaunched ? 1 : 0) - LabLaunchingProgress.Opacity,
|
||||
100),
|
||||
ModAnimation.AaOpacity(LabLaunchingProgressLeft,
|
||||
(!isLaunched ? 0.5d : 0d) - LabLaunchingProgressLeft.Opacity, 100),
|
||||
ModAnimation.AaOpacity(PanLaunchingHint,
|
||||
(isLaunched && shouldShowHint ? 1 : 0) - PanLaunchingHint.Opacity, 100)
|
||||
});
|
||||
}
|
||||
|
||||
ModAnimation.AniStart(animList, "Launching Progress");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Minecraft.Launch.Error.RefreshInfo"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Minecraft.Launch.Error.RefreshInfo"));
|
||||
}
|
||||
}
|
||||
|
||||
private void PanLaunchingInfo_SizeChangedW(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
var deltaWidth = e.NewSize.Width - e.PreviousSize.Width;
|
||||
if (e.PreviousSize.Width == 0d || isWidthAnimating || Math.Abs(deltaWidth) < 1d ||
|
||||
PanLaunchingInfo.ActualWidth == 0d)
|
||||
return;
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaWidth(PanLaunchingInfo, deltaWidth, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
isWidthAnimating = false;
|
||||
PanLaunchingInfo.Width = actualUsedWidth;
|
||||
}, after: true)
|
||||
}, "Launching Info Width");
|
||||
isWidthAnimating = true;
|
||||
actualUsedWidth = PanLaunchingInfo.Width;
|
||||
PanLaunchingInfo.Width = e.PreviousSize.Width;
|
||||
}
|
||||
|
||||
private void PanLaunchingInfo_SizeChangedH(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
var deltaHeight = e.NewSize.Height - e.PreviousSize.Height;
|
||||
if (e.PreviousSize.Height == 0d || isHeightAnimating || Math.Abs(deltaHeight) < 1d ||
|
||||
PanLaunchingInfo.ActualHeight == 0d)
|
||||
return;
|
||||
ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaHeight(PanLaunchingInfo, deltaHeight, 180, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
isHeightAnimating = false;
|
||||
PanLaunchingInfo.Height = actualUsedHeight;
|
||||
}, after: true)
|
||||
}, "Launching Info Height");
|
||||
isHeightAnimating = true;
|
||||
actualUsedHeight = PanLaunchingInfo.Height;
|
||||
PanLaunchingInfo.Height = e.PreviousSize.Height;
|
||||
}
|
||||
|
||||
// 启动游戏按钮
|
||||
private void BtnLaunch_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
LaunchButtonClick();
|
||||
}
|
||||
|
||||
#region 切换大页面
|
||||
|
||||
/// <summary>
|
||||
/// 切换至启动中页面。
|
||||
/// </summary>
|
||||
public void PageChangeToLaunching()
|
||||
{
|
||||
// 修改验证方式
|
||||
switch (ModProfile.selectedProfile.Type)
|
||||
{
|
||||
case ModLaunch.McLoginType.Legacy:
|
||||
{
|
||||
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.Offline");
|
||||
break;
|
||||
}
|
||||
case ModLaunch.McLoginType.Ms:
|
||||
{
|
||||
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.Microsoft");
|
||||
break;
|
||||
}
|
||||
case ModLaunch.McLoginType.Auth:
|
||||
{
|
||||
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.ThirdParty") + (!string.IsNullOrEmpty(ModProfile.selectedProfile.ServerName)
|
||||
? " / " + ModProfile.selectedProfile.ServerName
|
||||
: "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化页面
|
||||
LabLaunchingName.Text = ModInstanceList.McMcInstanceSelected.Name;
|
||||
LabLaunchingStage.Text = Lang.Text("Common.Action.Initialize");
|
||||
LabLaunchingTitle.Text = ModLaunch.currentLaunchOptions?.SaveBatch is null
|
||||
? Lang.Text("Launch.Status.Title.Launching")
|
||||
: Lang.Text("Launch.Status.Title.ExportingScript");
|
||||
LabLaunchingProgress.Text = Lang.Number(0d, "P2");
|
||||
LabLaunchingProgress.Opacity = 1d;
|
||||
LabLaunchingDownload.Visibility = Visibility.Visible;
|
||||
LabLaunchingProgressLeft.Opacity = 0.6d;
|
||||
LabLaunchingDownload.Visibility = Visibility.Visible;
|
||||
LabLaunchingDownload.Text = ModBase.GetString(0) + "/s";
|
||||
LabLaunchingDownload.Opacity = 0d;
|
||||
LabLaunchingDownload.Visibility = Visibility.Collapsed;
|
||||
LabLaunchingDownloadLeft.Opacity = 0d;
|
||||
LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed;
|
||||
ProgressLaunchingFinished.Width = new GridLength(0d, GridUnitType.Star);
|
||||
ProgressLaunchingUnfinished.Width = new GridLength(1d, GridUnitType.Star);
|
||||
PanLaunchingHint.Opacity = 0d;
|
||||
PanLaunchingHint.Visibility = Visibility.Collapsed;
|
||||
PanLaunchingInfo.Width = double.NaN; // 重置宽度改变动画
|
||||
ModLaunch.mcLaunchProcess = null;
|
||||
ModLaunch.mcLaunchWatcher = null;
|
||||
|
||||
var shouldShowHint = Config.Preference.ShowLaunchingHint;
|
||||
if (shouldShowHint)
|
||||
LabLaunchingHint.Text = PageLaunchRight.GetRandomHint(true, true);
|
||||
else
|
||||
LabLaunchingHint.Text = "";
|
||||
|
||||
// 初始化其他页面
|
||||
PanInput.IsHitTestVisible = false;
|
||||
PanLaunching.IsHitTestVisible = false;
|
||||
LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Run;
|
||||
PanLaunching.Visibility = Visibility.Visible;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PanInput, 0d, 50),
|
||||
ModAnimation.AaOpacity(PanInput, -PanInput.Opacity, 110, ease: new ModAnimation.AniEaseInFluent(),
|
||||
after: true),
|
||||
ModAnimation.AaScaleTransform(PanInput, 1.2d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 160),
|
||||
ModAnimation.AaOpacity(PanLaunching, 1d - PanLaunching.Opacity, 150, 100),
|
||||
ModAnimation.AaScaleTransform(PanLaunching, 1d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX,
|
||||
500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => PanLaunching.IsHitTestVisible = true, 150)
|
||||
}, "Launch State Page"); // 略作延迟,这样如果预检测失败,不会出现奇怪的弹一下的动画
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换至登录页面。
|
||||
/// </summary>
|
||||
public void PageChangeToLogin()
|
||||
{
|
||||
if (PageGet(pageCurrent) is ILoginPage loginPage) loginPage.Reload();
|
||||
PanInput.IsHitTestVisible = false;
|
||||
PanLaunching.IsHitTestVisible = false;
|
||||
LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Stop;
|
||||
PanInput.Visibility = Visibility.Visible;
|
||||
ModAnimation.AniStart(
|
||||
new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PanLaunching, -PanLaunching.Opacity, 150),
|
||||
ModAnimation.AaScaleTransform(PanLaunching,
|
||||
0.8d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX, 150,
|
||||
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaOpacity(PanInput, 1d - PanInput.Opacity, 250, 50),
|
||||
ModAnimation.AaScaleTransform(PanInput, 1d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 300, 50,
|
||||
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
|
||||
ModAnimation.AaCode(() => PanInput.IsHitTestVisible = true, 200)
|
||||
}, "Launch State Page", true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 切换登录页面
|
||||
|
||||
private enum PageType
|
||||
{
|
||||
None,
|
||||
Auth,
|
||||
Ms,
|
||||
Profile,
|
||||
ProfileSkin,
|
||||
Offline
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前页面的种类。
|
||||
/// </summary>
|
||||
private PageType pageCurrent = PageType.None;
|
||||
|
||||
private object PageGet(PageType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PageType.Auth:
|
||||
{
|
||||
if (ModMain.frmLoginAuth is null)
|
||||
ModMain.frmLoginAuth = new PageLoginAuth();
|
||||
return ModMain.frmLoginAuth;
|
||||
}
|
||||
case PageType.Ms:
|
||||
{
|
||||
if (ModMain.frmLoginMs is null)
|
||||
ModMain.frmLoginMs = new PageLoginMs();
|
||||
return ModMain.frmLoginMs;
|
||||
}
|
||||
case PageType.Profile:
|
||||
{
|
||||
if (ModMain.frmLoginProfile is null)
|
||||
ModMain.frmLoginProfile = new PageLoginProfile();
|
||||
return ModMain.frmLoginProfile;
|
||||
}
|
||||
case PageType.ProfileSkin:
|
||||
{
|
||||
if (ModMain.frmLoginProfileSkin is null)
|
||||
ModMain.frmLoginProfileSkin = new PageLoginProfileSkin();
|
||||
return ModMain.frmLoginProfileSkin;
|
||||
}
|
||||
case PageType.Offline:
|
||||
{
|
||||
if (ModMain.frmLoginOffline is null)
|
||||
ModMain.frmLoginOffline = new PageLoginOffline();
|
||||
return ModMain.frmLoginOffline;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("Type", "即将切换的登录分页编号越界");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换现有登录页面种类,返回新页面的实例。
|
||||
/// </summary>
|
||||
/// <param name="type">新页面的种类。</param>
|
||||
/// <param name="anim">是否显示动画。</param>
|
||||
private object PageChange(PageType type, bool anim)
|
||||
{
|
||||
object pageNew = ModMain.frmLoginMs; // 初始化一个东西,避免在执行时出现异常导致雪崩
|
||||
try
|
||||
{
|
||||
#region 确定更改的页面实例并实例化
|
||||
|
||||
if (pageCurrent == type)
|
||||
return pageNew;
|
||||
pageNew = PageGet(type);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 切换页面
|
||||
|
||||
ModAnimation.AniStop("FrmLogin PageChange");
|
||||
// 清除页面关联性
|
||||
if (pageNew is FrameworkElement element && element.Parent is not null)
|
||||
{
|
||||
element.SetValue(ContentPresenter.ContentProperty, null);
|
||||
}
|
||||
if (anim)
|
||||
{
|
||||
// 动画
|
||||
// 执行动画
|
||||
Dispatcher.Invoke(() => ModAnimation.AniStart(new[]
|
||||
{
|
||||
ModAnimation.AaOpacity(PanLogin, -PanLogin.Opacity, 100, ease: new ModAnimation.AniEaseOutFluent()),
|
||||
ModAnimation.AaCode(() =>
|
||||
{
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
PanLogin.Children.Clear();
|
||||
PanLogin.Children.Add((UIElement)pageNew);
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
}, 100),
|
||||
ModAnimation.AaOpacity(PanLogin, 1d, 100, 120, new ModAnimation.AniEaseInFluent())
|
||||
}, "FrmLogin PageChange"), DispatcherPriority.Render);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无动画
|
||||
ModAnimation.AniControlEnabled += 1;
|
||||
PanLogin.Children.Clear();
|
||||
PanLogin.Children.Add((UIElement)pageNew);
|
||||
ModAnimation.AniControlEnabled -= 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
pageCurrent = type;
|
||||
return pageNew;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Error.SwitchPage", ModBase.GetStringFromEnum(type)),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Launch.Account.Error.SwitchPage", ModBase.GetStringFromEnum(type)));
|
||||
return pageNew;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认当前显示的子页面正确,并刷新该页面。
|
||||
/// </summary>
|
||||
/// <param name="anim">是否显示动画</param>
|
||||
/// <param name="targetLoginType">目标验证方式,若正在创建档案需填</param>
|
||||
public void RefreshPage(bool anim, ModLaunch.McLoginType targetLoginType = default)
|
||||
{
|
||||
var type = default(PageType);
|
||||
if (targetLoginType != default)
|
||||
{
|
||||
if (targetLoginType == ModLaunch.McLoginType.Ms)
|
||||
type = PageType.Ms;
|
||||
if (targetLoginType == ModLaunch.McLoginType.Auth)
|
||||
type = PageType.Auth;
|
||||
if (targetLoginType == ModLaunch.McLoginType.Legacy)
|
||||
type = PageType.Offline;
|
||||
}
|
||||
else if (ModProfile.selectedProfile is not null)
|
||||
{
|
||||
type = PageType.ProfileSkin;
|
||||
BtnLaunch.IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
type = PageType.Profile;
|
||||
if (_launchButtonAction != LaunchButtonAction.Download)
|
||||
BtnLaunch.IsEnabled = false;
|
||||
}
|
||||
|
||||
// 刷新页面
|
||||
if (pageCurrent == type)
|
||||
return;
|
||||
PageChange(type, anim);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 皮肤
|
||||
|
||||
// 正版皮肤
|
||||
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinMs = new("Loader Skin Ms", SkinMsLoad,
|
||||
SkinMsInput, ThreadPriority.AboveNormal);
|
||||
|
||||
private static ModBase.EqualableList<string> SkinMsInput()
|
||||
{
|
||||
// 获取名称
|
||||
return new ModBase.EqualableList<string>
|
||||
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
|
||||
}
|
||||
|
||||
private static void SkinMsLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
|
||||
{
|
||||
// 清空已有皮肤
|
||||
// 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
|
||||
ModMain.frmLoginProfileSkin.Skin.Clear();
|
||||
});
|
||||
// 获取 Url
|
||||
var userName = data.input[0];
|
||||
var uuid = data.input[1];
|
||||
if (ModProfile.selectedProfile is not null)
|
||||
{
|
||||
userName = ModProfile.selectedProfile.Username;
|
||||
uuid = ModProfile.selectedProfile.Uuid;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/" + ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) +
|
||||
".png";
|
||||
ModBase.Log("[Minecraft] 获取微软正版皮肤失败,ID 为空");
|
||||
goto Finish;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = ModSkin.McSkinGetAddress(uuid, "Ms");
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException("当前任务已取消:" + userName);
|
||||
result = ModSkin.McSkinDownload(result);
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException("当前任务已取消:" + userName);
|
||||
data.output = result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is ThreadInterruptedException)
|
||||
{
|
||||
data.output = "";
|
||||
ModBase.Log("[Minecraft] 已取消皮肤获取:" + userName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ex.ToString().Contains("429"))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/" +
|
||||
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
|
||||
ModBase.Log(
|
||||
Lang.Text("Launch.Skin.Error.MsRateLimited", userName),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Error.MsRateLimited", userName));
|
||||
}
|
||||
else if (ex.ToString().Contains("未设置自定义皮肤"))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/" +
|
||||
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
|
||||
ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载");
|
||||
}
|
||||
else
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/" +
|
||||
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Error.MsGet", userName),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Error.MsGet", userName));
|
||||
}
|
||||
}
|
||||
|
||||
Finish: ;
|
||||
|
||||
// 刷新显示
|
||||
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
|
||||
ModBase.RunInUi(ModMain.frmLoginProfileSkin.Skin.Load);
|
||||
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
|
||||
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
|
||||
}
|
||||
|
||||
// 离线皮肤
|
||||
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinLegacy = new("Loader Skin Legacy",
|
||||
SkinLegacyLoad, SkinLegacyInput, ThreadPriority.AboveNormal);
|
||||
|
||||
private static ModBase.EqualableList<string> SkinLegacyInput()
|
||||
{
|
||||
return new ModBase.EqualableList<string>
|
||||
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
|
||||
}
|
||||
|
||||
private static void SkinLegacyLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
|
||||
{
|
||||
// 清空已有皮肤
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
|
||||
ModMain.frmLoginProfileSkin.Skin.Clear();
|
||||
});
|
||||
data.output = ModBase.pathImage + "Skins/" + ModSkin.McSkinSex(data.input[1]) + ".png";
|
||||
// 刷新显示
|
||||
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
|
||||
ModBase.RunInUi(() => ModMain.frmLoginProfileSkin.Skin.Load());
|
||||
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
|
||||
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
|
||||
}
|
||||
|
||||
// Authlib-Injector 皮肤
|
||||
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinAuth = new("Loader Skin Auth",
|
||||
SkinAuthLoad, SkinAuthInput, ThreadPriority.AboveNormal);
|
||||
|
||||
private static ModBase.EqualableList<string> SkinAuthInput()
|
||||
{
|
||||
// 获取名称
|
||||
return new ModBase.EqualableList<string>
|
||||
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
|
||||
}
|
||||
|
||||
private static void SkinAuthLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
|
||||
{
|
||||
// 清空已有皮肤
|
||||
// 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
|
||||
ModMain.frmLoginProfileSkin.Skin.Clear();
|
||||
});
|
||||
// 获取 Url
|
||||
var userName = data.input[0];
|
||||
var uuid = data.input[1];
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/Steve.png";
|
||||
ModBase.Log("[Minecraft] 获取 Authlib-Injector 皮肤失败,ID 为空");
|
||||
goto Finish;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = ModSkin.McSkinGetAddress(uuid, "Auth");
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException("当前任务已取消:" + userName);
|
||||
result = ModSkin.McSkinDownload(result);
|
||||
if (data.IsAborted)
|
||||
throw new ThreadInterruptedException("当前任务已取消:" + userName);
|
||||
data.output = result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is ThreadInterruptedException)
|
||||
{
|
||||
data.output = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (ex.ToString().Contains("429"))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/Steve.png";
|
||||
ModBase.Log(
|
||||
$"[Minecraft] 获取 Authlib-Injector 皮肤失败({userName}):获取皮肤太过频繁,请 5 分钟后再试!",
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Error.AuthlibRateLimited"));
|
||||
}
|
||||
else if (ex.ToString().Contains("未设置自定义皮肤"))
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/Steve.png";
|
||||
ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载");
|
||||
}
|
||||
else
|
||||
{
|
||||
data.output = ModBase.pathImage + "Skins/Steve.png";
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Skin.Error.AuthGet", userName),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Skin.Error.AuthGet", userName));
|
||||
}
|
||||
}
|
||||
|
||||
Finish: ;
|
||||
|
||||
// 刷新显示
|
||||
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
|
||||
ModBase.RunInUi(ModMain.frmLoginProfileSkin.Skin.Load);
|
||||
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
|
||||
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
|
||||
}
|
||||
|
||||
// 全部皮肤加载器
|
||||
// 需要放在其中元素的后面,否则会因为它提前被加载而莫名其妙变成 Nothing
|
||||
public static List<ModLoader.LoaderTask<ModBase.EqualableList<string>, string>> skinLoaders = new()
|
||||
{ skinMs, skinLegacy, skinAuth };
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<local:MyPageRight x:Class="PCL.PageLaunchRight"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:PCL"> <!-- 不知道为啥只有这个文件不能在 XAML 设置 PanScroll -->
|
||||
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
|
||||
<StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
|
||||
<StackPanel x:Name="PanCustom" />
|
||||
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Launch.Right.CommunityHint.Title}" x:Name="PanHint">
|
||||
<local:MyIconButton LogoScale="1.1"
|
||||
SvgIcon="lucide/x"
|
||||
Height="20" Width="20" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
x:Name="BtnHintClose"
|
||||
Click="BtnHintClose_Click" />
|
||||
<StackPanel Margin="25,38,23,15">
|
||||
<TextBlock x:Name="LabHint1" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
|
||||
<TextBlock x:Name="LabHint2" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
|
||||
</StackPanel>
|
||||
</local:MyCard>
|
||||
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Launch.Right.Log.Title}" x:Name="PanLog">
|
||||
<TextBlock x:Name="LabLog" Margin="20,38,20,18" TextTrimming="None" TextWrapping="Wrap" />
|
||||
</local:MyCard>
|
||||
</StackPanel>
|
||||
</local:MyScrollViewer>
|
||||
</local:MyPageRight>
|
||||
@@ -0,0 +1,898 @@
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.UI;
|
||||
using PCL.Network;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLaunchRight : IRefreshable
|
||||
{
|
||||
public PageLaunchRight()
|
||||
{
|
||||
InitializeComponent();
|
||||
onlineLoader = new ModLoader.LoaderTask<string, int>(
|
||||
Lang.Text("Launch.Homepage.Task.Download"),
|
||||
OnlineLoaderSub)
|
||||
{
|
||||
reloadTimeout = 10 * 60 * 1000
|
||||
};
|
||||
Loaded += (_, _) => Init();
|
||||
Loaded += (_, _) => Refresh();
|
||||
Unloaded += (_, _) => _DisposeHomepageLiveWatcher();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
PanBack.ScrollToHome();
|
||||
PanScroll = PanBack; // 不知道为啥不能在 XAML 设置
|
||||
PanLog.Visibility = ModBase.modeDebug ? Visibility.Visible : Visibility.Collapsed;
|
||||
// 社区版提示
|
||||
PanHint.Visibility = States.Hint.CEMessage
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
LabHint1.Text = Lang.Text("Launch.Right.CommunityHint.Message");
|
||||
LabHint2.Text = Lang.Text("Launch.Right.CommunityHint.HidePrompt");
|
||||
_EnsureHomepageLiveWatcher();
|
||||
}
|
||||
|
||||
// 暂时关闭快照版提示
|
||||
private void BtnHintClose_Click(object sender, EventArgs e)
|
||||
{
|
||||
var input = ModMain.MyMsgBoxInput(Lang.Text("Launch.Right.CommunityHint.InputTitle"));
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return;
|
||||
input = new string(input.Where(char.IsAsciiLetter).ToArray()).ToLower();
|
||||
if (input.Contains("pclcommunity"))
|
||||
{
|
||||
ModAnimation.AniDispose(PanHint, true);
|
||||
States.Hint.CEMessage = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Right.CommunityHint.WrongInput"));
|
||||
}
|
||||
}
|
||||
|
||||
#region 主页
|
||||
|
||||
/// <summary>
|
||||
/// 刷新主页。
|
||||
/// </summary>
|
||||
private void Refresh()
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (refreshLock)
|
||||
{
|
||||
RefreshReal();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
"加载 PCL 主页自定义信息失败",
|
||||
ModBase.modeDebug
|
||||
? ModBase.LogLevel.Msgbox
|
||||
: ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Error.OperationFailed"));
|
||||
}
|
||||
}, $"刷新主页 #{ModBase.GetUuid()}");
|
||||
}
|
||||
|
||||
private void RefreshReal()
|
||||
{
|
||||
var content = "";
|
||||
string url = null;
|
||||
|
||||
var uiCustomType = (int)Config.Preference.Homepage.Type;
|
||||
|
||||
if (uiCustomType == 1)
|
||||
{
|
||||
// 本地文件
|
||||
LogWrapper.Info("[Page] 主页自定义数据来源:本地文件");
|
||||
content = ModBase.ReadFile(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml"));
|
||||
}
|
||||
else if (uiCustomType == 2)
|
||||
{
|
||||
// 网络文件
|
||||
url = (string)Config.Preference.Homepage.CustomUrl;
|
||||
content = LoadFromNetwork(url);
|
||||
}
|
||||
else if (uiCustomType == 3)
|
||||
{
|
||||
// 预设主页
|
||||
var preset = (int)Config.Preference.Homepage.SelectedPreset;
|
||||
switch (preset)
|
||||
{
|
||||
case 0:
|
||||
LogWrapper.Info("[Page] 主页预设:你知道吗");
|
||||
var hintText = GetRandomHint();
|
||||
content = $@"
|
||||
<local:MyCard Title=""{{DynamicResource Launch.Status.Trivia}}"" Margin=""0,0,0,15"">
|
||||
<TextBlock Margin=""25,38,23,15"" FontSize=""13.5"" IsHitTestVisible=""False"" Text=""{hintText}"" TextWrapping=""Wrap"" Foreground=""{{DynamicResource ColorBrush1}}"" />
|
||||
<local:MyIconButton Height=""22"" Width=""22"" Margin=""9"" VerticalAlignment=""Top"" HorizontalAlignment=""Right""
|
||||
EventType=""刷新主页"" EventData=""/""
|
||||
SvgIcon=""lucide/refresh-cw"" />
|
||||
</local:MyCard>";
|
||||
break;
|
||||
|
||||
case 1:
|
||||
LogWrapper.Info("[Page] 主页预设:回声洞 已被移除");
|
||||
ModMain.MyMsgBox(Lang.Text("Launch.Homepage.Preset.EchoCave.Removed"));
|
||||
return;
|
||||
|
||||
case 2:
|
||||
LogWrapper.Info("[Page] 主页预设:Minecraft 新闻");
|
||||
url = "https://news.bugjump.net";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
// case 3:
|
||||
// LogWrapper.Info("[Page] 主页预设:简单主页");
|
||||
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/MFn233/Custom.xaml";
|
||||
// content = LoadFromNetwork(url);
|
||||
// break;
|
||||
|
||||
case 3:
|
||||
LogWrapper.Info("[Page] 主页预设:每日整合包推荐");
|
||||
url = "https://pclsub.sodamc.com/";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
LogWrapper.Info("[Page] 主页预设:Minecraft 皮肤推荐");
|
||||
url = "https://forgepixel.com/pcl_sub_file";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
LogWrapper.Info("[Page] 主页预设:OpenBMCLAPI 仪表盘 Lite");
|
||||
url = "https://pcl-bmcl.milu.ink/";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
// case 7:
|
||||
// LogWrapper.Info("[Page] 主页预设:主页市场");
|
||||
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/JingHai-Lingyun/Custom.xaml";
|
||||
// content = LoadFromNetwork(url);
|
||||
// break;
|
||||
|
||||
// case 8:
|
||||
// LogWrapper.Info("[Page] 主页预设:更新日志");
|
||||
// url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Joker2184/UpdateHomepage.xaml";
|
||||
// content = LoadFromNetwork(url);
|
||||
// break;
|
||||
|
||||
case 6:
|
||||
LogWrapper.Info("[Page] 主页预设:PCL 新功能说明书");
|
||||
url = "https://raw.gitcode.com/WForst-Breeze/WhatsNewPCL/raw/main/Custom.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
// case 10:
|
||||
// LogWrapper.Info("[Page] 主页预设:OpenMCIM Dashboard");
|
||||
// url = "https://files.mcimirror.top/PCL";
|
||||
// content = LoadFromNetwork(url);
|
||||
// break;
|
||||
|
||||
case 7:
|
||||
LogWrapper.Info("[Page] 主页预设:杂志主页");
|
||||
url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Ext1nguisher/Custom.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 8:
|
||||
LogWrapper.Info("[Page] 主页预设:PCL GitHub 仪表盘");
|
||||
url = "https://ddf.pcl-community.org/Custom.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 9:
|
||||
LogWrapper.Info("[Page] 主页预设:Minecraft 更新摘要");
|
||||
url = "https://raw.gitcode.com/ENC_Euphony/PCL-AI-Summary-HomePage/raw/master/Custom.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 10:
|
||||
LogWrapper.Info("[Page] 主页预设:今日新闻热点");
|
||||
url = "https://pcl.wyc-w.top/index.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 11:
|
||||
LogWrapper.Info("[Page] 主页预设:Minecraft 芝士站");
|
||||
url = "https://www.xxag.top/mkss";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 12:
|
||||
LogWrapper.Info("[Page] 主页预设:整合包推荐引擎");
|
||||
url = "https://qawsedrftgyhujiko.fun/pcl2/Custom.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 13:
|
||||
LogWrapper.Info("[Page] 主页预设:Bangumi 番剧主页");
|
||||
url = "https://bangumi.p.kaphia.qzz.io";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 14:
|
||||
LogWrapper.Info("[Page] 主页预设:PCL CE 公告栏");
|
||||
url = "https://s3.pysio.online/pcl2-ce/apiv2/pages/announce.xaml";
|
||||
content = LoadFromNetwork(url);
|
||||
break;
|
||||
|
||||
case 15:
|
||||
LogWrapper.Info("[Page] 主页预设:Minecraft 信息流");
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (ModMain.frmHomepageNews is null)
|
||||
ModMain.frmHomepageNews = new PageHomepageNewsView();
|
||||
PanCustom.Children.Clear();
|
||||
PanCustom.Children.Add(ModMain.frmHomepageNews);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ModBase.RunInUi(() => LoadContent(content));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 URL 加载网络内容,优先使用缓存
|
||||
/// </summary>
|
||||
private string LoadFromNetwork(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return "";
|
||||
|
||||
var cachePath = Path.Combine(ModBase.pathTemp, "Cache", "Custom.xaml");
|
||||
var cachedUrl = (string)States.UI.SavedHomepageUrl;
|
||||
|
||||
if (url == cachedUrl && File.Exists(cachePath))
|
||||
{
|
||||
LogWrapper.Info("[Page] 主页自定义数据来源:联网缓存文件");
|
||||
// 后台更新缓存
|
||||
onlineLoader.Start(url);
|
||||
return ModBase.ReadFile(cachePath);
|
||||
}
|
||||
|
||||
LogWrapper.Info("[Page] 主页自定义数据来源:联网全新下载");
|
||||
HintWrapper.Show(Lang.Text("Launch.Homepage.Loading"));
|
||||
ModBase.RunInUiWait(() => LoadContent("")); // 先清空页面
|
||||
States.UI.SavedHomepageVersion = "";
|
||||
onlineLoader.Start(url); // 下载完成后将会再次触发更新
|
||||
return "";
|
||||
}
|
||||
|
||||
private readonly object refreshLock = new();
|
||||
|
||||
public static string GetRandomHint(bool enableLengthLimit = false, bool raw = false)
|
||||
{
|
||||
string[]? lines = null;
|
||||
|
||||
// 外部文件
|
||||
var externalPath = Path.Combine(ModBase.exePath, "PCL", "hints.txt");
|
||||
if (File.Exists(externalPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
lines = File.ReadAllLines(externalPath)
|
||||
.Where(l => !string.IsNullOrWhiteSpace(l))
|
||||
.Select(l => l.Trim())
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModBase.Log(
|
||||
Lang.Text("Launch.Homepage.Error.ExternalFile", externalPath),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Homepage.Error.ExternalFile", externalPath));
|
||||
}
|
||||
}
|
||||
|
||||
// 嵌入式资源
|
||||
if (lines is null || lines.Length == 0)
|
||||
{
|
||||
var langCode = LocalizationService.CurrentLanguage.Code;
|
||||
lines = _LoadEmbeddedHints(langCode)
|
||||
?? _LoadEmbeddedHints(LocalizationService.DefaultLanguageCode);
|
||||
}
|
||||
|
||||
// 长度限制
|
||||
if (enableLengthLimit)
|
||||
{
|
||||
var shortLines = lines.Where(l => l.Length < 50).ToArray();
|
||||
if (shortLines.Length > 0) lines = shortLines;
|
||||
}
|
||||
|
||||
// 随机返回
|
||||
var hint = lines[Random.Shared.Next(lines.Length)];
|
||||
return raw ? hint : hint.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
||||
}
|
||||
|
||||
private static string[]? _LoadEmbeddedHints(string langCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"pack://application:,,,/Plain Craft Launcher 2;component/Resources/hints/{langCode}.txt", UriKind.Absolute);
|
||||
using var stream = Application.GetResourceStream(uri)?.Stream;
|
||||
if (stream is null) return null;
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd()
|
||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(l => !string.IsNullOrWhiteSpace(l))
|
||||
.Select(l => l.Trim())
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 联网获取主页文件
|
||||
private readonly ModLoader.LoaderTask<string, int> onlineLoader;
|
||||
|
||||
private void OnlineLoaderSub(ModLoader.LoaderTask<string, int> task)
|
||||
{
|
||||
var address = task.input; // #3721 中连续触发两次导致内容变化
|
||||
try
|
||||
{
|
||||
// 获取版本校验地址
|
||||
string versionAddress;
|
||||
if (address.Contains(".xaml"))
|
||||
{
|
||||
versionAddress = address.Replace(".xaml", ".xaml.ini");
|
||||
}
|
||||
else
|
||||
{
|
||||
versionAddress = address.BeforeFirst("?");
|
||||
if (!versionAddress.EndsWith("/"))
|
||||
versionAddress += "/";
|
||||
versionAddress += "version";
|
||||
if (address.Contains("?"))
|
||||
versionAddress += "?" + address.AfterFirst("?");
|
||||
}
|
||||
|
||||
// 校验版本
|
||||
var version = "";
|
||||
var needDownload = true;
|
||||
try
|
||||
{
|
||||
version = Requester.FetchString(versionAddress);
|
||||
if (version.Length > 1000)
|
||||
throw new Exception($"获取的主页版本过长({version.Length} 字符)");
|
||||
var currentVersion = States.UI.SavedHomepageVersion;
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(currentVersion) &&
|
||||
(version ?? "") == (currentVersion ?? ""))
|
||||
{
|
||||
ModBase.Log($"[Page] 当前缓存的主页已为最新,当前版本:{version},检查源:{versionAddress}");
|
||||
needDownload = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log($"[Page] 需要下载联网主页,当前版本:{version},检查源:{versionAddress}");
|
||||
}
|
||||
}
|
||||
catch (Exception exx)
|
||||
{
|
||||
ModBase.Log(exx, "联网获取主页版本失败", ModBase.LogLevel.Developer);
|
||||
ModBase.Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{versionAddress}");
|
||||
}
|
||||
|
||||
// 实际下载
|
||||
if (needDownload)
|
||||
{
|
||||
var fileContent = Requester.FetchString(address);
|
||||
ModBase.Log($"[Page] 已联网下载主页,内容长度:{fileContent.Length},来源:{address}");
|
||||
States.UI.SavedHomepageUrl = address;
|
||||
States.UI.SavedHomepageVersion = version;
|
||||
ModBase.WriteFile(ModBase.pathTemp + @"Cache\Custom.xaml", fileContent);
|
||||
}
|
||||
|
||||
// 要求刷新
|
||||
ModBase.RunInUi(Refresh); // 不直接调用 Refresh,以防止死循环(#6245)
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Homepage.Error.Download", address),
|
||||
ModBase.modeDebug
|
||||
? ModBase.LogLevel.Msgbox
|
||||
: ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Homepage.Error.Download", address));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即强制刷新主页。
|
||||
/// 必须在 UI 线程调用。
|
||||
/// </summary>
|
||||
public void ForceRefresh()
|
||||
{
|
||||
ModBase.Log("[Page] 要求强制刷新主页");
|
||||
ClearCache();
|
||||
// 实际的刷新
|
||||
if (ModMain.frmMain.pageCurrent.page == FormMain.PageType.Launch)
|
||||
{
|
||||
PanBack.ScrollToHome();
|
||||
Refresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.Launch);
|
||||
}
|
||||
}
|
||||
|
||||
void IRefreshable.Refresh()
|
||||
{
|
||||
ForceRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空主页缓存信息。
|
||||
/// </summary>
|
||||
private void ClearCache()
|
||||
{
|
||||
loadedContentHash = -1;
|
||||
onlineLoader.input = "";
|
||||
States.UI.SavedHomepageUrl = "";
|
||||
States.UI.SavedHomepageVersion = "";
|
||||
ModBase.Log("[Page] 已清空主页缓存");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文本内容中加载主页。
|
||||
/// 必须在 UI 线程调用。
|
||||
/// </summary>
|
||||
private void LoadContent(string content)
|
||||
{
|
||||
lock (loadContentLock)
|
||||
{
|
||||
// 如果加载目标内容一致则不加载
|
||||
var hash = content.GetHashCode();
|
||||
if (hash == loadedContentHash)
|
||||
{
|
||||
_ApplyHomepageLivePatchesFromFile();
|
||||
return;
|
||||
}
|
||||
loadedContentHash = hash;
|
||||
// 实际加载内容
|
||||
PanCustom.Children.Clear();
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
ModBase.Log("[Page] 实例化:清空主页 UI,来源为空");
|
||||
return;
|
||||
}
|
||||
|
||||
var loadStartTime = DateTime.Now;
|
||||
try
|
||||
{
|
||||
content = ModMain.ArgumentReplace(content);
|
||||
while (content.Contains("xmlns"))
|
||||
content = content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", "");
|
||||
content =
|
||||
$"<StackPanel xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:sys=\"clr-namespace:System;assembly=System.Runtime\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:local=\"clr-namespace:PCL;assembly=Plain Craft Launcher 2\">{content}</StackPanel>";
|
||||
ModBase.Log($"[Page] 实例化:加载主页 UI 开始,最终内容长度:{content.Count()}");
|
||||
PanCustom.Children.Add((UIElement)ModBase.GetObjectFromXML(content, out var sanitizeResult));
|
||||
_ShowSanitizeHints(sanitizeResult);
|
||||
_ApplyHomepageLivePatchesFromFile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ModBase.modeDebug)
|
||||
{
|
||||
ModBase.Log(ex, $"加载失败的主页内容:\r\n{content}");
|
||||
if (ModMain.MyMsgBox(
|
||||
ex is UnauthorizedAccessException
|
||||
? ex.Message
|
||||
: Lang.Text("Launch.Homepage.LoadFailed.Message", ex),
|
||||
Lang.Text("Launch.Homepage.LoadFailed.Title"),
|
||||
Lang.Text("Launch.Homepage.LoadFailed.Retry"),
|
||||
Lang.Text("Common.Action.Cancel")) ==
|
||||
1) goto Refresh; // 防止 SyncLock 死锁
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Homepage.LoadFailed.Title"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Launch.Homepage.LoadFailed.Title"));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var loadCostTime = (DateTime.Now - loadStartTime).Milliseconds;
|
||||
ModBase.Log($"[Page] 实例化:加载主页 UI 完成,耗时 {loadCostTime}ms");
|
||||
if (loadCostTime > 3000)
|
||||
HintService.Hint(Lang.Text("Launch.Homepage.SlowWarning", Lang.Number(Math.Round(loadCostTime / 1000d, 1), "N1")));
|
||||
}
|
||||
|
||||
return;
|
||||
Refresh: ;
|
||||
|
||||
ForceRefresh();
|
||||
}
|
||||
|
||||
private int loadedContentHash = -1;
|
||||
private readonly object loadContentLock = new();
|
||||
private static void _ShowSanitizeHints(XamlEventSanitizer.SanitizeResult result)
|
||||
{
|
||||
foreach (var unsupported in result.UnsupportedTypesFound)
|
||||
HintService.Hint(Lang.Text("Event.Sanitize.UnsupportedTypeHint", unsupported), HintType.Error);
|
||||
|
||||
foreach (var unknown in result.UnrecognizedTypes)
|
||||
HintService.Hint(Lang.Text("Event.Sanitize.UnknownTypeHint", unknown), HintType.Error);
|
||||
}
|
||||
|
||||
private const string homepageLivePatchFileName = "CustomLive.json";
|
||||
private const string homepageLiveSupportFileName = "CustomLive.supported.json";
|
||||
// Keep the reflection patch surface explicit because patch files are written by external tools.
|
||||
private static readonly Dictionary<string, string> _homepageLiveAllowedProperties = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["text"] = "Text",
|
||||
["title"] = "Title",
|
||||
["info"] = "Info",
|
||||
["tooltip"] = "ToolTip",
|
||||
["visibility"] = "Visibility",
|
||||
["isEnabled"] = "IsEnabled",
|
||||
["opacity"] = "Opacity"
|
||||
};
|
||||
private FileSystemWatcher? _homepageLiveWatcher;
|
||||
private DispatcherTimer? _homepageLivePatchTimer;
|
||||
|
||||
private void _EnsureHomepageLiveWatcher()
|
||||
{
|
||||
if (_homepageLiveWatcher != null) return;
|
||||
if ((int)Config.Preference.Homepage.Type != 1) return;
|
||||
|
||||
try
|
||||
{
|
||||
var directory = _GetHomepageLiveDirectory();
|
||||
Directory.CreateDirectory(directory);
|
||||
_WriteHomepageLiveSupportMarker(directory);
|
||||
|
||||
_homepageLiveWatcher = new FileSystemWatcher(directory, homepageLivePatchFileName)
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName
|
||||
};
|
||||
_homepageLiveWatcher.Changed += (_, _) => _QueueHomepageLivePatchApply();
|
||||
_homepageLiveWatcher.Created += (_, _) => _QueueHomepageLivePatchApply();
|
||||
_homepageLiveWatcher.Renamed += (_, _) => _QueueHomepageLivePatchApply();
|
||||
_homepageLiveWatcher.EnableRaisingEvents = true;
|
||||
_QueueHomepageLivePatchApply();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to start custom homepage live patch watcher", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
private void _DisposeHomepageLiveWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
_homepageLiveWatcher?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch watcher", ModBase.LogLevel.Developer);
|
||||
}
|
||||
|
||||
_homepageLiveWatcher = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (_homepageLivePatchTimer != null)
|
||||
{
|
||||
_homepageLivePatchTimer.Stop();
|
||||
_homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick;
|
||||
_homepageLivePatchTimer = null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to dispose custom homepage live patch debounce timer", ModBase.LogLevel.Developer);
|
||||
}
|
||||
|
||||
_DeleteHomepageLiveSupportMarker();
|
||||
}
|
||||
|
||||
private void _QueueHomepageLivePatchApply()
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
_homepageLivePatchTimer ??= new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(120)
|
||||
};
|
||||
_homepageLivePatchTimer.Tick -= _HomepageLivePatchTimerTick;
|
||||
_homepageLivePatchTimer.Tick += _HomepageLivePatchTimerTick;
|
||||
_homepageLivePatchTimer.Stop();
|
||||
_homepageLivePatchTimer.Start();
|
||||
});
|
||||
}
|
||||
|
||||
private void _HomepageLivePatchTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
_homepageLivePatchTimer?.Stop();
|
||||
_ApplyHomepageLivePatchesFromFile();
|
||||
}
|
||||
|
||||
private void _ApplyHomepageLivePatchesFromFile()
|
||||
{
|
||||
if (PanCustom.Children.Count == 0) return;
|
||||
if ((int)Config.Preference.Homepage.Type != 1) return;
|
||||
|
||||
var file = Path.Combine(_GetHomepageLiveDirectory(), homepageLivePatchFileName);
|
||||
if (!File.Exists(file)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var token = JsonNode.Parse(_ReadHomepageLivePatchFile(file),
|
||||
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
|
||||
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true });
|
||||
foreach (var patch in _EnumerateHomepageLivePatches(token))
|
||||
_ApplyHomepageLivePatch(patch);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to apply custom homepage live patches", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
private static string _ReadHomepageLivePatchFile(string file)
|
||||
{
|
||||
Exception? lastException = null;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastException = ex;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastException ?? new IOException("Unable to read custom homepage live patch file.");
|
||||
}
|
||||
|
||||
private static string _GetHomepageLiveDirectory()
|
||||
{
|
||||
return Path.Combine(ModBase.exePath, "PCL");
|
||||
}
|
||||
|
||||
private static void _WriteHomepageLiveSupportMarker(string directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var marker = new JsonObject(new JsonNodeOptions { PropertyNameCaseInsensitive = true })
|
||||
{
|
||||
["processId"] = Environment.ProcessId,
|
||||
["processPath"] = Environment.ProcessPath ?? "",
|
||||
["patchFile"] = homepageLivePatchFileName,
|
||||
["startedAt"] = DateTime.Now.ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
File.WriteAllText(Path.Combine(directory, homepageLiveSupportFileName), marker.ToJsonString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to write custom homepage live patch support marker", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void _DeleteHomepageLiveSupportMarker()
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = Path.Combine(_GetHomepageLiveDirectory(), homepageLiveSupportFileName);
|
||||
if (!File.Exists(file)) return;
|
||||
|
||||
var marker = (JsonObject)JsonNode.Parse(_ReadHomepageLivePatchFile(file),
|
||||
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
|
||||
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true })!;
|
||||
if (marker["processId"]?.GetValue<int>() == Environment.ProcessId)
|
||||
File.Delete(file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Page] Failed to delete custom homepage live patch support marker", ModBase.LogLevel.Developer);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<JsonObject> _EnumerateHomepageLivePatches(JsonNode token)
|
||||
{
|
||||
if (token is JsonObject obj)
|
||||
{
|
||||
if (obj["patches"] is JsonArray patches)
|
||||
{
|
||||
foreach (var patch in patches.OfType<JsonObject>())
|
||||
yield return patch;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (_TryGetString(obj, "target", "tag", "name") != null)
|
||||
{
|
||||
yield return obj;
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var property in obj)
|
||||
{
|
||||
if (property.Value is not JsonObject patch) continue;
|
||||
patch = (JsonObject)JsonNode.Parse(patch.ToJsonString(),
|
||||
new JsonNodeOptions { PropertyNameCaseInsensitive = true },
|
||||
new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true })!;
|
||||
patch["target"] ??= property.Key;
|
||||
yield return patch;
|
||||
}
|
||||
}
|
||||
else if (token is JsonArray array)
|
||||
{
|
||||
foreach (var patch in array.OfType<JsonObject>())
|
||||
yield return patch;
|
||||
}
|
||||
}
|
||||
|
||||
private void _ApplyHomepageLivePatch(JsonObject patch)
|
||||
{
|
||||
var target = _TryGetString(patch, "target", "tag", "name");
|
||||
if (string.IsNullOrWhiteSpace(target)) return;
|
||||
|
||||
foreach (var element in _FindElementsByTag(PanCustom, target))
|
||||
_ApplyHomepageLivePatchToElement(element, patch);
|
||||
}
|
||||
|
||||
private void _ApplyHomepageLivePatchToElement(FrameworkElement element, JsonObject patch)
|
||||
{
|
||||
_SetPropertyIfPresent(element, patch, "text", "Text");
|
||||
_SetPropertyIfPresent(element, patch, "title", "Title");
|
||||
_SetPropertyIfPresent(element, patch, "info", "Info");
|
||||
_SetPropertyIfPresent(element, patch, "tooltip", "ToolTip");
|
||||
_SetPropertyIfPresent(element, patch, "toolTip", "ToolTip");
|
||||
_SetPropertyIfPresent(element, patch, "visibility", "Visibility");
|
||||
_SetPropertyIfPresent(element, patch, "isEnabled", "IsEnabled");
|
||||
_SetPropertyIfPresent(element, patch, "opacity", "Opacity");
|
||||
|
||||
if (patch["properties"] is JsonObject properties)
|
||||
{
|
||||
foreach (var property in properties)
|
||||
_TrySetElementProperty(element, property.Key, property.Value?.ToString() ?? "");
|
||||
}
|
||||
|
||||
var childrenXaml = _TryGetString(patch, "childrenXaml", "ChildrenXaml");
|
||||
if (!string.IsNullOrEmpty(childrenXaml) && element is Panel panel)
|
||||
_ReplacePanelChildren(panel, childrenXaml);
|
||||
}
|
||||
|
||||
private static void _SetPropertyIfPresent(FrameworkElement element, JsonObject patch, string jsonName, string propertyName)
|
||||
{
|
||||
if (patch.TryGetPropertyValue(jsonName, out var value))
|
||||
_TrySetElementProperty(element, propertyName, value?.ToString() ?? "");
|
||||
}
|
||||
|
||||
private static bool _TrySetElementProperty(FrameworkElement element, string propertyName, string value)
|
||||
{
|
||||
if (!_homepageLiveAllowedProperties.TryGetValue(propertyName, out var allowedPropertyName))
|
||||
{
|
||||
ModBase.Log($"[Page] Skipped unsupported live patch property {propertyName}", ModBase.LogLevel.Developer);
|
||||
return false;
|
||||
}
|
||||
|
||||
propertyName = allowedPropertyName;
|
||||
var property = element.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property == null || !property.CanWrite) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var trimmedValue = value.Trim();
|
||||
object convertedValue;
|
||||
if (propertyType == typeof(string))
|
||||
convertedValue = value;
|
||||
else if (propertyType == typeof(object))
|
||||
convertedValue = value;
|
||||
else if (propertyType == typeof(bool) && bool.TryParse(trimmedValue, out var boolValue))
|
||||
convertedValue = boolValue;
|
||||
else if (propertyType == typeof(int) && int.TryParse(trimmedValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||
convertedValue = intValue;
|
||||
else if (propertyType == typeof(double) && double.TryParse(trimmedValue, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
convertedValue = doubleValue;
|
||||
else if (propertyType == typeof(Visibility))
|
||||
{
|
||||
if (!Enum.TryParse(trimmedValue, true, out Visibility visibilityValue))
|
||||
return false;
|
||||
convertedValue = visibilityValue;
|
||||
}
|
||||
else if (propertyType.IsEnum && Enum.TryParse(propertyType, trimmedValue, true, out var enumValue))
|
||||
convertedValue = enumValue;
|
||||
else
|
||||
return false;
|
||||
|
||||
property.SetValue(element, convertedValue);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Page] Failed to set live patch property {propertyName}", ModBase.LogLevel.Developer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void _ReplacePanelChildren(Panel panel, string childrenXaml)
|
||||
{
|
||||
var content = ModMain.ArgumentReplace(childrenXaml);
|
||||
while (content.Contains("xmlns"))
|
||||
content = content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", "");
|
||||
|
||||
var wrapped =
|
||||
$"<StackPanel xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:sys=\"clr-namespace:System;assembly=System.Runtime\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:local=\"clr-namespace:PCL;assembly=Plain Craft Launcher 2\">{content}</StackPanel>";
|
||||
|
||||
if (ModBase.GetObjectFromXML(wrapped) is not Panel parsedPanel) return;
|
||||
|
||||
var children = parsedPanel.Children.OfType<UIElement>().ToList();
|
||||
parsedPanel.Children.Clear();
|
||||
panel.Children.Clear();
|
||||
foreach (var child in children)
|
||||
panel.Children.Add(child);
|
||||
}
|
||||
|
||||
private static IEnumerable<FrameworkElement> _FindElementsByTag(DependencyObject root, string tag)
|
||||
{
|
||||
if (root is FrameworkElement element &&
|
||||
string.Equals(element.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
|
||||
yield return element;
|
||||
|
||||
int count;
|
||||
try
|
||||
{
|
||||
count = VisualTreeHelper.GetChildrenCount(root);
|
||||
}
|
||||
catch
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
foreach (var child in _FindElementsByTag(VisualTreeHelper.GetChild(root, i), tag))
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? _TryGetString(JsonObject obj, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (obj.TryGetPropertyValue(name, out var value))
|
||||
return value?.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<Grid x:Class="PCL.PageLoginAuth" Tag="False"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d" d:DesignWidth="317.6" Margin="0,0,0,-2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="20" />
|
||||
<RowDefinition Height="30" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="TextServerName" Text="{DynamicResource Launch.Account.Auth.ServerLabel}" Grid.Row="0"
|
||||
Grid.Column="1" Margin="-40,0,0,0"
|
||||
HorizontalAlignment="Center" Visibility="Hidden" />
|
||||
<local:MyIconButton ToolTip="{DynamicResource Launch.Account.Auth.ChangeProfile}" Grid.Row="4" Height="14"
|
||||
Opacity="0" x:Name="BtnEdit"
|
||||
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32"
|
||||
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="0"
|
||||
HorizontalAlignment="Center"
|
||||
SvgIcon="lucide/pencil"
|
||||
Margin="8,0,0,0" VerticalAlignment="Center" />
|
||||
<local:MyIconButton ToolTip="{DynamicResource Launch.Account.Auth.Logout}" Grid.Row="4" Height="14" Opacity="0"
|
||||
x:Name="BtnExit"
|
||||
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32"
|
||||
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
SvgIcon="lucide/log-out"
|
||||
Margin="0,0,8,0" VerticalAlignment="Center" />
|
||||
<local:MyComboBox Grid.Row="2" Grid.Column="1" Height="28" x:Name="TextServer" IsEditable="True"
|
||||
IsTextSearchEnabled="False" />
|
||||
<TextBlock Text="{DynamicResource Launch.Account.Auth.Server}" Grid.Row="2" VerticalAlignment="Center" />
|
||||
<local:MyTextBox Grid.Row="4" Grid.Column="1" Height="28" x:Name="TextName" />
|
||||
<TextBlock Text="{DynamicResource Launch.Account.Auth.Email}" Grid.Row="4" VerticalAlignment="Center" />
|
||||
<PasswordBox Grid.Row="6" Grid.Column="1" x:Name="TextPass" />
|
||||
<TextBlock Text="{DynamicResource Launch.Account.Auth.Password}" Grid.Row="6" VerticalAlignment="Center" />
|
||||
<local:MyTextButton Margin="0,12,2,0" x:Name="BtnLink" Content="{DynamicResource Launch.Account.Auth.Register}"
|
||||
Grid.Row="7" HorizontalAlignment="Right"
|
||||
Grid.Column="1" Padding="20,0,0,0" HorizontalContentAlignment="Right" FontSize="13"
|
||||
VerticalContentAlignment="Center" Visibility="Collapsed" />
|
||||
<local:MyButton x:Name="BtnBack" Grid.Row="9" Grid.Column="1" Width="50" Margin="-100,0,0,0" ColorType="Normal"
|
||||
Text="{DynamicResource Launch.Account.Auth.Back}" HorizontalAlignment="Center" />
|
||||
<local:MyButton x:Name="BtnLogin" Grid.Row="9" Grid.Column="1" Width="50" Margin="20,0,0,0" ColorType="Highlight"
|
||||
Text="{DynamicResource Launch.Account.Auth.Login}" HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
@@ -0,0 +1,210 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
using PCL.Core.Minecraft.Yggdrasil;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using PCL.Core.Utils.Validate;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLoginAuth
|
||||
{
|
||||
public static string draggedAuthServer;
|
||||
|
||||
// 预设服务器
|
||||
private static readonly Dictionary<string, string> predefinedAuthServers = new()
|
||||
{
|
||||
{ Lang.Text("Launch.Account.Auth.Preset.LittleSkin"), "https://littleskin.cn/api/yggdrasil" },
|
||||
{ Lang.Text("Common.Option.Customize"), "" }
|
||||
};
|
||||
|
||||
private bool _isRegisterMode = true;
|
||||
|
||||
public PageLoginAuth()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => Reload();
|
||||
Loaded += (_, _) => ReloadRegisterButton();
|
||||
// Handles
|
||||
BtnBack.Click += BtnBack_Click;
|
||||
BtnLogin.Click += BtnLogin_Click;
|
||||
TextServer.TextChanged += TextServer_TextChanged;
|
||||
BtnLink.Click += Btn_Click;
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
var serverItems = TextServer.Items;
|
||||
serverItems.Clear();
|
||||
foreach (var serverName in predefinedAuthServers.Keys)
|
||||
serverItems.Add(new MyComboBoxItem { Content = serverName });
|
||||
TextServer.Text = draggedAuthServer;
|
||||
draggedAuthServer = null;
|
||||
}
|
||||
|
||||
private void BtnBack_Click(object sender, EventArgs e)
|
||||
{
|
||||
TextServer.Text = null;
|
||||
TextName.Text = null;
|
||||
TextPass.Password = null;
|
||||
ModMain.frmLaunchLeft.RefreshPage(true);
|
||||
}
|
||||
|
||||
private void BtnLogin_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TextServer.Text) || string.IsNullOrWhiteSpace(TextName.Text) ||
|
||||
string.IsNullOrWhiteSpace(TextPass.Password))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.Auth.EmptyFields"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TextServer.Text.IsMatch(RegexPatterns.HttpUri))
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.Auth.InvalidServer"), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
BtnLogin.IsEnabled = false;
|
||||
BtnBack.IsEnabled = false;
|
||||
var loginData = new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
BaseUrl = TextServer.Text.EndsWithF("/") ? $"{TextServer.Text}authserver" : $"{TextServer.Text}/authserver",
|
||||
UserName = TextName.Text, Password = TextPass.Password, Description = "Authlib-Injector",
|
||||
LoginType = ModLaunch.McLoginType.Auth
|
||||
};
|
||||
Dispatcher.BeginInvoke(new Func<Task>(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ModProfile.isCreatingProfile = true;
|
||||
ModLaunch.mcLoginAuthLoader.Start(loginData, true);
|
||||
while (ModLaunch.mcLoginAuthLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
BtnLogin.Text = Lang.Number(ModLaunch.mcLoginAuthLoader.Progress, "P0");
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
switch (ModLaunch.mcLoginAuthLoader.State)
|
||||
{
|
||||
case ModBase.LoadState.Finished:
|
||||
ModMain.frmLaunchLeft.RefreshPage(true);
|
||||
break;
|
||||
case ModBase.LoadState.Aborted:
|
||||
HintService.Hint(Lang.Text("Launch.Account.Auth.Cancelled"));
|
||||
break;
|
||||
case ModBase.LoadState.Waiting:
|
||||
case ModBase.LoadState.Loading:
|
||||
case ModBase.LoadState.Failed:
|
||||
default:
|
||||
{
|
||||
if (ModLaunch.mcLoginAuthLoader.Error is null)
|
||||
throw new Exception(Lang.Text("Launch.Account.Microsoft.Error.Unknown"));
|
||||
throw new Exception(ModLaunch.mcLoginAuthLoader.Error.Message,
|
||||
ModLaunch.mcLoginAuthLoader.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message == "$$")
|
||||
{
|
||||
}
|
||||
else if (ex.Message.StartsWith("$"))
|
||||
{
|
||||
HintService.Hint(
|
||||
Lang.Text("Launch.Account.Auth.LoginFailed.WithDetail",ex.Message.TrimStart('$')),
|
||||
HintType.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Auth.LoginFailed"),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Account.Auth.LoginFailed"));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ModProfile.isCreatingProfile = false;
|
||||
BtnLogin.IsEnabled = true;
|
||||
BtnBack.IsEnabled = true;
|
||||
BtnLogin.Text = Lang.Text("Launch.Account.Auth.Login");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取验证服务器名称
|
||||
private void GetServerName()
|
||||
{
|
||||
var serverUriInput = TextServer.Text;
|
||||
if (string.IsNullOrWhiteSpace(serverUriInput))
|
||||
{
|
||||
TextServerName.Visibility = Visibility.Hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.BeginInvoke(async () =>
|
||||
{
|
||||
string serverUri = null;
|
||||
string serverName = null;
|
||||
try
|
||||
{
|
||||
serverUri = await ApiLocation.TryRequestAsync(serverUriInput);
|
||||
using var resp = await HttpRequest.Create(serverUri).SendAsync();
|
||||
var responseText = await resp.AsStringAsync();
|
||||
serverName = await Task.Run(() => ModBase.GetJson(responseText)["meta"]?["serverName"]?.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "从服务器获取名称失败");
|
||||
}
|
||||
|
||||
if (serverUri is not null) TextServer.Text = serverUri;
|
||||
if (serverName is null)
|
||||
{
|
||||
TextServerName.Visibility = Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
TextServerName.Text = Lang.Text("Launch.Account.Auth.ServerLabel", serverName);
|
||||
TextServerName.Visibility = Visibility.Visible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 链接处理
|
||||
private void ComboName_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
_isRegisterMode = string.IsNullOrEmpty(TextName.Text);
|
||||
BtnLink.Content = _isRegisterMode
|
||||
? Lang.Text("Launch.Account.Auth.Register")
|
||||
: Lang.Text("Launch.Account.Auth.ForgotPassword");
|
||||
}
|
||||
|
||||
private void Btn_Click(object sender, EventArgs e)
|
||||
{
|
||||
ModBase.OpenWebsite(_isRegisterMode
|
||||
? Config.InstanceAuth.AuthRegisterAddress.ToString()
|
||||
: Config.InstanceAuth.AuthRegisterAddress.ToString().Replace("/auth/register", "/auth/forgot"));
|
||||
}
|
||||
|
||||
// 切换注册按钮可见性
|
||||
private void ReloadRegisterButton()
|
||||
{
|
||||
var address = Config.InstanceAuth.AuthRegisterAddress.ToString();
|
||||
BtnLink.Visibility = new HttpValidator().Validate(address).IsValid
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void TextServer_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
predefinedAuthServers.TryGetValue(TextServer.Text, out var server);
|
||||
if (server is not null) TextServer.Text = server;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<StackPanel x:Class="PCL.PageLoginMs" Tag="False" Orientation="Vertical"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d" d:DesignWidth="302.4">
|
||||
<Grid x:Name="PanEmpty" Margin="0,10,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="25" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Path
|
||||
Data="M660.338 528.065c63.61-46.825 105.131-121.964 105.131-206.83 0-141.7-115.29-256.987-256.997-256.987-141.706 0-256.998 115.288-256.998 256.987 0 85.901 42.52 161.887 107.456 208.562-152.1 59.92-260.185 207.961-260.185 381.077 0 21.276 17.253 38.53 38.53 38.53 21.278 0 38.53-17.254 38.53-38.53 0-183.426 149.232-332.671 332.667-332.671 1.589 0 3.113-0.207 4.694-0.244 0.8 0.056 1.553 0.244 2.362 0.244 183.434 0 332.664 149.245 332.664 332.671 0 21.276 17.255 38.53 38.533 38.53 21.277 0 38.53-17.254 38.53-38.53 0-174.885-110.354-324.13-264.917-382.809z m-331.803-206.83c0-99.22 80.72-179.927 179.935-179.927s179.937 80.708 179.937 179.927c0 99.203-80.721 179.91-179.937 179.91s-179.935-80.708-179.935-179.91z"
|
||||
Fill="{DynamicResource ColorBrush2}" Opacity="0.7" Width="40" Stretch="Uniform" Grid.ColumnSpan="2"
|
||||
Margin="4,0,0,0" />
|
||||
<local:MyButton Text="{DynamicResource Launch.Account.Microsoft.Start}" x:Name="BtnLogin" ColorType="Highlight"
|
||||
Height="28" Width="100" Margin="0,0,0,0" Grid.Row="2" Grid.Column="0" />
|
||||
<Grid Grid.Row="4" Grid.ColumnSpan="2" Opacity="0.3" Margin="2,30,2,0" HorizontalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="30" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:MyTextButton Text="{DynamicResource Launch.Account.Microsoft.Purchase}"
|
||||
local:CustomEventService.EventType="OpenUrl"
|
||||
local:CustomEventService.EventData="https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"
|
||||
Grid.Column="0" />
|
||||
<local:MyTextButton Text="{DynamicResource Launch.Account.Microsoft.Website}"
|
||||
local:CustomEventService.EventType="OpenUrl"
|
||||
local:CustomEventService.EventData="https://www.minecraft.net/zh-hans"
|
||||
Grid.Column="2" />
|
||||
</Grid>
|
||||
<local:MyTextButton x:Name="BtnBack" Text="{DynamicResource Launch.Account.Back}" Grid.Row="5"
|
||||
Grid.ColumnSpan="2" HorizontalAlignment="Center"
|
||||
Margin="0,60,0,0" Opacity="0.3" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Security.Authentication;
|
||||
using System.Windows;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLoginMs
|
||||
{
|
||||
public PageLoginMs()
|
||||
{
|
||||
// Handles
|
||||
InitializeComponent();
|
||||
BtnBack.Click += BtnBack_Click;
|
||||
BtnLogin.Click += BtnLogin_Click;
|
||||
}
|
||||
|
||||
private void BtnBack_Click(object sender, EventArgs e)
|
||||
{
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
|
||||
}
|
||||
|
||||
private void BtnLogin_Click(object sender, EventArgs e)
|
||||
{
|
||||
BtnLogin.IsEnabled = false;
|
||||
BtnBack.Visibility = Visibility.Collapsed;
|
||||
BtnLogin.Text = Lang.Number(0d, "P0");
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ModProfile.selectedProfile = null;
|
||||
ModLaunch.mcLoginMsLoader.Start(ModProfile.GetLoginData(ModLaunch.McLoginType.Ms), true);
|
||||
while (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Loading)
|
||||
{
|
||||
ModBase.RunInUi(() => BtnLogin.Text = Lang.Number(ModLaunch.mcLoginMsLoader.Progress, "P0"));
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
|
||||
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Finished)
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
|
||||
else if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Aborted)
|
||||
throw new ThreadInterruptedException();
|
||||
else if (ModLaunch.mcLoginMsLoader.Error is null)
|
||||
throw new Exception(Lang.Text("Launch.Account.Microsoft.Error.Unknown"));
|
||||
else
|
||||
throw new Exception(ModLaunch.mcLoginMsLoader.Error.Message, ModLaunch.mcLoginMsLoader.Error);
|
||||
}
|
||||
catch (ThreadInterruptedException ex)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.LoginCancelled"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.Message == "$$")
|
||||
{
|
||||
}
|
||||
else if (ex.Message.StartsWith("$"))
|
||||
{
|
||||
HintService.Hint(
|
||||
Lang.Text("Launch.Account.Microsoft.LoginFailed.WithDetail", ex.Message.TrimStart('$')),
|
||||
HintType.Error);
|
||||
}
|
||||
else if (ex is AuthenticationException && ex.Message.ContainsF("SSL/TLS"))
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
$"{Lang.Text("Launch.Account.Microsoft.LoginFailed.Message")}\r\n{ex.Message}",
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Account.Microsoft.Error.OperationFailed"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Microsoft.LoginFailed.Title"),
|
||||
ModBase.LogLevel.Msgbox,
|
||||
userSummary: Lang.Text("Launch.Account.Microsoft.LoginFailed.Title"));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
BtnLogin.IsEnabled = true;
|
||||
BtnBack.Visibility = Visibility.Visible;
|
||||
BtnLogin.Text = Lang.Text("Launch.Account.Login");
|
||||
});
|
||||
}
|
||||
}, "Ms Login");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<Grid x:Class="PCL.PageLoginOffline" Tag="False"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d" d:DesignWidth="317.6" Margin="0,0,0,-2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="30" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="30" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="50" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{DynamicResource Launch.Account.Offline.PlayerId}" Grid.Row="0" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
<local:MyTextBox Grid.Row="0" Grid.Column="1" Height="28" x:Name="TextName"
|
||||
ToolTip="{DynamicResource Launch.Account.Offline.PlayerId.ToolTip}" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="{DynamicResource Launch.Account.Offline.UuidStandard}"
|
||||
TextAlignment="Center" Margin="-45,0,0,0" FontWeight="Bold" />
|
||||
<Grid Margin="-40,0,0,0" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="85" />
|
||||
<ColumnDefinition Width="85" />
|
||||
<ColumnDefinition Width="85" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<local:MyRadioBox Grid.Column="0" Text="{DynamicResource Launch.Account.Offline.Uuid.Standard}"
|
||||
x:Name="RadioUuidStandard" Checked="True" Height="22"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" Check="RadioUuid_Checked"
|
||||
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Standard.ToolTip}" />
|
||||
<local:MyRadioBox Grid.Column="1" Text="{DynamicResource Launch.Account.Offline.Uuid.Legacy}"
|
||||
x:Name="RadioUuidLegacy" Height="22" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" Check="RadioUuid_Checked"
|
||||
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Legacy.ToolTip}" />
|
||||
<local:MyRadioBox Grid.Column="2" Text="{DynamicResource Common.Option.Customize}" x:Name="RadioUuidCustom"
|
||||
Height="22" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" Check="RadioUuid_Checked"
|
||||
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Custom.ToolTip}" />
|
||||
</Grid>
|
||||
<TextBlock Text="UUID" Grid.Row="6" x:Name="TextUuidTitle" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
<local:MyTextBox Grid.Row="6" Grid.Column="1" Height="28" Visibility="Collapsed" x:Name="TextUuid"
|
||||
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Custom.Input.ToolTip}" />
|
||||
<local:MyButton x:Name="BtnBack" Grid.Row="9" Grid.Column="1" Width="50" Margin="-100,-40,0,40" ColorType="Normal"
|
||||
Text="{DynamicResource Launch.Account.Offline.Back}" HorizontalAlignment="Center" />
|
||||
<local:MyButton x:Name="BtnLogin" Grid.Row="9" Grid.Column="1" Width="50" Margin="20,-40,0,40"
|
||||
ColorType="Highlight" Text="{DynamicResource Launch.Account.Offline.Create}"
|
||||
HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Windows;
|
||||
using PCL.Core.Utils.Validate;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLoginOffline
|
||||
{
|
||||
public PageLoginOffline()
|
||||
{
|
||||
// Handles
|
||||
InitializeComponent();
|
||||
BtnBack.Click += BtnBack_Click;
|
||||
RadioUuidCustom.Check += RadioUuid_Checked;
|
||||
RadioUuidStandard.Check += RadioUuid_Checked;
|
||||
RadioUuidLegacy.Check += RadioUuid_Checked;
|
||||
BtnLogin.Click += BtnLogin_Click;
|
||||
}
|
||||
|
||||
private void BtnBack_Click(object sender, EventArgs e)
|
||||
{
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
|
||||
}
|
||||
|
||||
private void RadioUuid_Checked(object sender, ModBase.RouteEventArgs e)
|
||||
{
|
||||
if (RadioUuidCustom.Checked)
|
||||
{
|
||||
TextUuidTitle.Visibility = Visibility.Visible;
|
||||
TextUuid.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
TextUuidTitle.Visibility = Visibility.Collapsed;
|
||||
TextUuid.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnLogin_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 玩家 ID 输入检查
|
||||
var username = TextName.Text;
|
||||
var usernameValidateResult = new RegexValidator("^[A-z0-9_]{3,16}$").Validate(username);
|
||||
if (!usernameValidateResult.IsValid)
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Launch.Account.Offline.InvalidPlayerId.Message"),
|
||||
Lang.Text("Launch.Account.Offline.InvalidPlayerId.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"), isWarn: true, forceWait: true) == 2)
|
||||
return;
|
||||
// UUID
|
||||
string userUuid = null;
|
||||
if (RadioUuidCustom.Checked)
|
||||
{
|
||||
// 自定义输入检查
|
||||
var uuidInput = TextUuid.Text.Replace("-", "");
|
||||
var uuidValidateResult = new RegexValidator("^[a-fA-F0-9]{32}$").Validate(uuidInput);
|
||||
if (RadioUuidCustom.Checked && !uuidValidateResult.IsValid)
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.Offline.InvalidUuid", uuidValidateResult), HintType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
userUuid = uuidInput;
|
||||
}
|
||||
else if (RadioUuidLegacy.Checked)
|
||||
{
|
||||
userUuid = ModProfile.GetOfflineUuid(username, isLegacy: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
userUuid = ModProfile.GetOfflineUuid(username);
|
||||
}
|
||||
|
||||
// 创建档案
|
||||
var newProfile = new ModProfile.McProfile
|
||||
{
|
||||
Type = ModLaunch.McLoginType.Legacy,
|
||||
Uuid = userUuid,
|
||||
Username = username,
|
||||
Desc = ""
|
||||
};
|
||||
ModProfile.profileList.Add(newProfile);
|
||||
ModProfile.SaveProfile();
|
||||
ModProfile.selectedProfile = newProfile;
|
||||
ModProfile.isCreatingProfile = false;
|
||||
HintService.Hint(Lang.Text("Launch.Account.Profile.Created"), HintType.Success);
|
||||
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<Grid x:Class="PCL.PageLoginProfile" Tag="False"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Self}}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="20" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<local:MyHint x:Name="HintSelect" Margin="0,10,0,0" Theme="Blue" Text="{DynamicResource Launch.Account.Profile.SelectHint}" Grid.Row="0" CanClose="True"
|
||||
RelativeSetup="HintProfileSelect" />
|
||||
<local:MyHint x:Name="HintCreate" Margin="0,10,0,0" Theme="Blue" Text="{DynamicResource Launch.Account.Profile.CreateAndSelectHint}" Grid.Row="0"
|
||||
Visibility="Collapsed" />
|
||||
<local:MyHint x:Name="HintMicrosoft" Margin="0,10,0,0" Theme="Yellow" Text="{DynamicResource Launch.Account.Profile.RequireMicrosoftHint}" Grid.Row="1"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<local:MyScrollViewer Grid.Row="3" MaxHeight="300" Margin="10,0,10,0">
|
||||
<!-- 世界需要 MVVM -->
|
||||
<ItemsControl ItemsSource="{Binding ProfileCollection}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:MyListItem Margin="8,2,10,2"
|
||||
Title="{Binding Username}" Info="{Binding Info}"
|
||||
Type="Clickable" Logo="{Binding Logo}" SvgIcon="{Binding SvgIcon}" Tag="{Binding Profile}"
|
||||
Click="SelectProfile" ContentHandler="ProfileContMenuBuild" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</local:MyScrollViewer>
|
||||
|
||||
<local:MyCard Grid.Row="5" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="10"
|
||||
CornerRadius="5" Margin="0,0,4,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="10,3,0,3">
|
||||
<local:MyIconButton Height="24" Margin="0,0.5,9,0" x:Name="BtnNew"
|
||||
Click="BtnNew_Click"
|
||||
ToolTip="{DynamicResource Launch.Account.Profile.Create}" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
|
||||
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
|
||||
LogoScale="1.08"
|
||||
SvgIcon="lucide/user-plus" />
|
||||
</StackPanel>
|
||||
</local:MyCard>
|
||||
|
||||
</Grid>
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.UI;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLoginProfile
|
||||
{
|
||||
public PageLoginProfile()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => Reload();
|
||||
}
|
||||
|
||||
public ObservableCollection<ProfileItem> ProfileCollection { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 刷新页面显示的所有信息。
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
RefreshProfileList();
|
||||
ModMain.frmLoginProfileSkin = null;
|
||||
// RunInNewThread(Sub()
|
||||
// Thread.Sleep(800)
|
||||
// RunInUi(Sub() FrmLaunchLeft.RefreshPage(True))
|
||||
// End Sub)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新档案列表
|
||||
/// </summary>
|
||||
public void RefreshProfileList()
|
||||
{
|
||||
ModBase.Log("[Profile] 刷新档案列表");
|
||||
ProfileCollection.Clear();
|
||||
ModProfile.GetProfile();
|
||||
try
|
||||
{
|
||||
foreach (var p in ModProfile.profileList)
|
||||
ProfileCollection.Add(new ProfileItem(p));
|
||||
HintMicrosoft.Visibility = ModProfile.profileList.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ModBase.Log("[Profile] 档案列表刷新完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Launch.Account.Profile.Error.Read"),
|
||||
ModBase.LogLevel.Feedback,
|
||||
userSummary: Lang.Text("Launch.Account.Profile.Error.Read"));
|
||||
}
|
||||
|
||||
if (!ModProfile.profileList.Any())
|
||||
{
|
||||
States.Hint.LaunchWithProfile = true;
|
||||
HintCreate.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
HintCreate.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public class ProfileItem
|
||||
{
|
||||
public ProfileItem(ModProfile.McProfile profile)
|
||||
{
|
||||
Profile = profile;
|
||||
Info = (string)ModProfile.GetProfileInfo(profile);
|
||||
var logoPath = ModBase.pathTemp + $@"Cache\Skin\Head\{profile.SkinHeadId}.png";
|
||||
if (File.Exists(logoPath) && new FileInfo(logoPath).Length != 0L)
|
||||
{
|
||||
Logo = logoPath;
|
||||
SvgIcon = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logo = string.Empty;
|
||||
SvgIcon = "lucide/user";
|
||||
}
|
||||
}
|
||||
|
||||
public string Info { get; private set; }
|
||||
public string Logo { get; private set; } = string.Empty;
|
||||
public string SvgIcon { get; private set; } = string.Empty;
|
||||
public ModProfile.McProfile Profile { get; }
|
||||
public string Username => Profile.Username;
|
||||
}
|
||||
|
||||
#region 控件
|
||||
|
||||
private void SelectProfile(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var item = (MyListItem)sender;
|
||||
var tag = (ModProfile.McProfile)item.Tag;
|
||||
ModProfile.selectedProfile = (ModProfile.McProfile)((MyListItem)sender).Tag;
|
||||
ModBase.Log($"[Profile] 选定档案: {tag.Username}, 以 {tag.Type} 方式验证");
|
||||
ModProfile.lastUsedProfile =
|
||||
ModProfile.profileList.IndexOf((ModProfile.McProfile)((MyListItem)sender).Tag); // 获取当前档案的序号
|
||||
ModProfile.SaveProfile(); // 保存档案配置,确保切换后的档案被正确保存
|
||||
|
||||
// 清除登录验证缓存,确保使用新档案的验证信息
|
||||
ModLaunch.mcLoginMsLoader.State = ModBase.LoadState.Waiting;
|
||||
ModLaunch.mcLoginAuthLoader.State = ModBase.LoadState.Waiting;
|
||||
ModLaunch.mcLoginLegacyLoader.State = ModBase.LoadState.Waiting;
|
||||
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
ModMain.frmLaunchLeft.RefreshPage(true);
|
||||
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void ProfileContMenuBuild(MyListItem sender, EventArgs e)
|
||||
{
|
||||
// 更改 UUID
|
||||
var btnEditUuid = new MyIconButton
|
||||
{ SvgIcon = "lucide/pencil", ToolTip = Lang.Text("Launch.Account.Profile.ChangeUuid"), Tag = sender.Tag };
|
||||
ToolTipService.SetPlacement(btnEditUuid, PlacementMode.Center);
|
||||
ToolTipService.SetVerticalOffset(btnEditUuid, 30d);
|
||||
ToolTipService.SetHorizontalOffset(btnEditUuid, 2d);
|
||||
btnEditUuid.Click += EditProfileUuid;
|
||||
// 复制 UUID
|
||||
var btnCopyUuid = new MyIconButton
|
||||
{ SvgIcon = "lucide/copy", ToolTip = Lang.Text("Launch.Account.Profile.CopyUuid"), Tag = sender.Tag };
|
||||
ToolTipService.SetPlacement(btnCopyUuid, PlacementMode.Center);
|
||||
ToolTipService.SetVerticalOffset(btnCopyUuid, 30d);
|
||||
ToolTipService.SetHorizontalOffset(btnCopyUuid, 2d);
|
||||
btnCopyUuid.Click += CopyProfileUuid;
|
||||
// 更改验证服务器名称
|
||||
var btnEditServerName = new MyIconButton
|
||||
{ SvgIcon = "lucide/info", ToolTip = Lang.Text("Launch.Account.Profile.ChangeAuthServerName"), Tag = sender.Tag };
|
||||
ToolTipService.SetPlacement(btnEditServerName, PlacementMode.Center);
|
||||
ToolTipService.SetVerticalOffset(btnEditServerName, 30d);
|
||||
ToolTipService.SetHorizontalOffset(btnEditServerName, 2d);
|
||||
btnEditServerName.Click += EditProfileServer;
|
||||
// 删除档案
|
||||
var btnDelete = new MyIconButton { SvgIcon = "lucide/trash-2", ToolTip = Lang.Text("Launch.Account.Profile.Delete"), Tag = sender.Tag };
|
||||
ToolTipService.SetPlacement(btnDelete, PlacementMode.Center);
|
||||
ToolTipService.SetVerticalOffset(btnDelete, 30d);
|
||||
ToolTipService.SetHorizontalOffset(btnDelete, 2d);
|
||||
btnDelete.Click += DeleteProfile;
|
||||
// 根据档案类型显示不同的菜单项
|
||||
if (((ModProfile.McProfile)sender.Tag).Type == ModLaunch.McLoginType.Legacy)
|
||||
sender.Buttons = new[] { btnEditUuid, btnDelete };
|
||||
else
|
||||
sender.Buttons = new[] { btnCopyUuid, btnDelete };
|
||||
}
|
||||
|
||||
// 创建档案
|
||||
private void BtnNew_Click(object sender, EventArgs e)
|
||||
{
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
ModProfile.CreateProfile();
|
||||
ModBase.RunInUi(() => RefreshProfileList());
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑 UUID
|
||||
private void EditProfileUuid(object sender, EventArgs e)
|
||||
{
|
||||
ModProfile.EditOfflineUuid((ModProfile.McProfile)((MyIconButton)sender).Tag);
|
||||
}
|
||||
|
||||
private void CopyProfileUuid(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is MyIconButton { Tag: ModProfile.McProfile profile }) ModBase.ClipboardSet(profile.Uuid);
|
||||
}
|
||||
|
||||
// 编辑验证服务器名称
|
||||
private void EditProfileServer(object sender, EventArgs e)
|
||||
{
|
||||
var profile = (ModProfile.McProfile)((MyIconButton)sender).Tag;
|
||||
string name = ModMain.MyMsgBoxInput(Lang.Text("Launch.Account.Profile.EditServerName.Title"), Lang.Text("Launch.Account.Profile.EditServerName.Message"), profile.ServerName);
|
||||
if (name is not null) ModProfile.EditAuthServerName(profile, name);
|
||||
}
|
||||
|
||||
// 删除档案
|
||||
private void DeleteProfile(object sender, EventArgs e)
|
||||
{
|
||||
if (ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.DeleteConfirm.Message"), Lang.Text("Launch.Account.Profile.DeleteConfirm.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"), isWarn: true,
|
||||
forceWait: true) == 2)
|
||||
return;
|
||||
ModProfile.RemoveProfile((ModProfile.McProfile)((MyIconButton)sender).Tag);
|
||||
ModBase.RunInUi(() => RefreshProfileList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<Grid x:Class="PCL.PageLoginProfileSkin" Tag="False"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:PCL"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<Grid Margin="0,0,0,0" Background="{StaticResource ColorBrushTransparent}" Name="PanData">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="34" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="80" />
|
||||
<RowDefinition Height="Auto" MinHeight="30" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Grid.Column="1" Grid.Row="1" VerticalAlignment="Top">
|
||||
<local:MySkin HorizontalAlignment="Center" VerticalAlignment="Top" x:Name="Skin" HasCape="False"
|
||||
Margin="0,0,0,10" IsHitTestVisible="False" />
|
||||
<TextBlock Name="TextName" Text="Username" Margin="8,0" FontSize="16" HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Name="TextType" Text="AuthType" Margin="8,4,8,5" FontSize="12" HorizontalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" Foreground="{DynamicResource ColorBrushGray4}" />
|
||||
</StackPanel>
|
||||
<local:MyCard Grid.Column="1" Grid.Row="2" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="0"
|
||||
CornerRadius="5" Margin="0,8,0,0">
|
||||
<StackPanel Orientation="Horizontal" Margin="10,3,9,3">
|
||||
<local:MyIconButton Height="24" Margin="0,0.5,9,0" x:Name="BtnSkin"
|
||||
ToolTip="{DynamicResource Launch.Account.ProfileSkin.SkinsAndCapes}"
|
||||
ToolTipService.Placement="Center"
|
||||
ToolTipService.VerticalOffset="35" ToolTipService.HorizontalOffset="1"
|
||||
ToolTipService.InitialShowDelay="50"
|
||||
LogoScale="1.08"
|
||||
SvgIcon="lucide/shirt">
|
||||
<local:MyIconButton.ContextMenu>
|
||||
<ContextMenu Closed="MenuAccountOptions_Closed" HorizontalOffset="10" VerticalOffset="18">
|
||||
<local:MyMenuItem Click="Skin_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.ChangeSkin}" />
|
||||
<local:MyMenuItem Click="BtnSkinSave_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.SaveSkin}" />
|
||||
<local:MyMenuItem Click="BtnSkinRefresh_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.RefreshSkin}" />
|
||||
<local:MyMenuItem Click="BtnSkinCape_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.ChangeCape}" />
|
||||
</ContextMenu>
|
||||
</local:MyIconButton.ContextMenu>
|
||||
</local:MyIconButton>
|
||||
<local:MyIconButton Height="24" Margin="0,0,9,0" x:Name="BtnEdit"
|
||||
ToolTip="{DynamicResource Launch.Account.ProfileSkin.EditProfile}"
|
||||
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
|
||||
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
|
||||
LogoScale="1.1"
|
||||
SvgIcon="lucide/pencil">
|
||||
<local:MyIconButton.ContextMenu>
|
||||
<ContextMenu Closed="MenuAccountOptions_Closed" HorizontalOffset="10" VerticalOffset="18">
|
||||
<local:MyMenuItem Click="BtnEditPassword_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.EditPassword}" />
|
||||
<local:MyMenuItem Click="BtnEditName_Click"
|
||||
Header="{DynamicResource Launch.Account.ProfileSkin.EditUsername}" />
|
||||
</ContextMenu>
|
||||
</local:MyIconButton.ContextMenu>
|
||||
</local:MyIconButton>
|
||||
<local:MyIconButton Height="24" x:Name="BtnSelect" Margin="0,0,0,0" Click="ChangeProfile"
|
||||
ToolTip="{DynamicResource Launch.Account.ProfileSkin.SwitchProfile}"
|
||||
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
|
||||
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
|
||||
LogoScale="1.15"
|
||||
SvgIcon="lucide/arrow-right-left" />
|
||||
</StackPanel>
|
||||
</local:MyCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public partial class PageLoginProfileSkin
|
||||
{
|
||||
public PageLoginProfileSkin()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => Reload();
|
||||
// Handles
|
||||
PanData.MouseEnter += ShowPanel;
|
||||
PanData.MouseLeave += HidePanel;
|
||||
BtnSkin.Click += BtnSkin_Click;
|
||||
BtnEdit.Click += BtnEdit_Click;
|
||||
BtnSelect.Click += ChangeProfile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新页面显示的所有信息。
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
ModBase.Log("[Profile] 刷新档案界面");
|
||||
Skin.Clear();
|
||||
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Visible;
|
||||
ModBase.Log("[Profile] 使用正版皮肤加载器");
|
||||
Skin.loader = PageLaunchLeft.skinMs;
|
||||
}
|
||||
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Visible;
|
||||
ModBase.Log("[Profile] 使用 Authlib 皮肤加载器");
|
||||
Skin.loader = PageLaunchLeft.skinAuth;
|
||||
}
|
||||
else
|
||||
{
|
||||
BtnEdit.Visibility = Visibility.Collapsed;
|
||||
ModBase.Log("[Profile] 使用离线皮肤加载器");
|
||||
Skin.loader = PageLaunchLeft.skinLegacy;
|
||||
}
|
||||
|
||||
Skin.loader.Start(isForceRestart: true);
|
||||
TextName.Text = ModProfile.selectedProfile.Username;
|
||||
TextType.Text = (string)ModProfile.GetProfileInfo(ModProfile.selectedProfile);
|
||||
}
|
||||
|
||||
#region 控制与编辑
|
||||
|
||||
// 显示 / 隐藏控制
|
||||
private void ShowPanel(object sender, MouseEventArgs e)
|
||||
{
|
||||
ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, 1d - PanButtons.Opacity, 120),
|
||||
"PageLoginProfileSkin Button");
|
||||
}
|
||||
|
||||
private void HidePanel(object sender, EventArgs e)
|
||||
{
|
||||
if (BtnEdit.ContextMenu.IsOpen || BtnSkin.ContextMenu.IsOpen || PanData.IsMouseOver)
|
||||
return;
|
||||
ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, -PanButtons.Opacity, 120),
|
||||
"PageLoginProfileSkin Button");
|
||||
}
|
||||
|
||||
private void MenuAccountOptions_Closed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HidePanel(sender, e);
|
||||
}
|
||||
|
||||
// 皮肤与披风子菜单
|
||||
private void BtnSkin_Click(object sender, EventArgs e)
|
||||
{
|
||||
BtnSkin.ContextMenu.IsOpen = true;
|
||||
}
|
||||
|
||||
// 账号信息子菜单
|
||||
private void BtnEdit_Click(object sender, EventArgs e)
|
||||
{
|
||||
BtnEdit.ContextMenu.IsOpen = true;
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
private void BtnEditPassword_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
|
||||
{
|
||||
ModBase.OpenWebsite("https://account.live.com/password/Change");
|
||||
}
|
||||
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
|
||||
{
|
||||
var server = ModProfile.selectedProfile.Server;
|
||||
ModBase.OpenWebsite(server.Replace("/api/yggdrasil/authserver" + (server.EndsWithF("/") ? "/" : ""),
|
||||
"/user/profile"));
|
||||
}
|
||||
else
|
||||
{
|
||||
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.PasswordUnsupported"));
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 ID
|
||||
private void BtnEditName_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ModProfile.EditProfileId();
|
||||
}
|
||||
|
||||
// 选择档案
|
||||
private void ChangeProfile(object sender, EventArgs e)
|
||||
{
|
||||
ModProfile.selectedProfile = null;
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
ModMain.frmLaunchLeft.RefreshPage(true);
|
||||
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 修改皮肤
|
||||
private void Skin_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
|
||||
ModProfile.ChangeSkinMs();
|
||||
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
|
||||
ModBase.OpenWebsite(ModProfile.selectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") +
|
||||
"user/closet");
|
||||
else
|
||||
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.SkinUnsupported"));
|
||||
}
|
||||
|
||||
// 保存皮肤
|
||||
private void BtnSkinSave_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Skin.BtnSkinSave_Click(sender, e);
|
||||
}
|
||||
|
||||
// 刷新皮肤
|
||||
private void BtnSkinRefresh_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Skin.RefreshClick(sender, e);
|
||||
}
|
||||
|
||||
// 修改披风
|
||||
private void BtnSkinCape_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
|
||||
Skin.BtnSkinCape_Click(sender, e);
|
||||
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
|
||||
ModBase.OpenWebsite(ModProfile.selectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") +
|
||||
"user/closet");
|
||||
else
|
||||
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.CapeUnsupported"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user