feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,113 @@
<Grid
x:Class="PCL.MyLocalCompItem"
x:Name="PanBack"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL;assembly="
Height="44"
Background="{StaticResource ColorBrushSemiTransparent}"
RenderTransformOrigin="0.5,0.5"
SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="6" />
<ColumnDefinition Width="34" />
<ColumnDefinition Width="7" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition x:Name="ColumnPaddingRight" Width="4" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="20" />
<RowDefinition Height="18" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<!-- Logo -->
<local:MyImage
x:Name="PathLogo"
Grid.Row="1"
Grid.RowSpan="2"
Grid.Column="1"
Width="34"
Height="34"
HorizontalAlignment="Right"
VerticalAlignment="Center"
CornerRadius="6"
FallbackSource="pack://application:,,,/images/Icons/NoIcon.png"
IsHitTestVisible="False"
RenderOptions.BitmapScalingMode="HighQuality"
SnapsToDevicePixels="True" />
<!-- 标题 -->
<Grid
x:Name="PanTitle"
Grid.Row="1"
Grid.Column="3"
Grid.ColumnSpan="2"
Margin="0,1,0,0"
VerticalAlignment="Center"
SnapsToDevicePixels="False"
UseLayoutRounding="False">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnTitle" Width="Auto" />
<ColumnDefinition x:Name="ColumnSubtitle" Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition x:Name="ColumnExtend" Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock
x:Name="LabTitle"
FontSize="14"
IsHitTestVisible="False"
TextTrimming="CharacterEllipsis" />
<TextBlock
x:Name="LabSubtitle"
Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource ColorBrushGray1}"
IsHitTestVisible="False"
Opacity="0.4"
TextTrimming="CharacterEllipsis"
Visibility="Collapsed" />
<!-- 更新按钮 -->
<local:MyIconButton
x:Name="BtnUpdate"
Grid.Column="2"
Width="21"
Height="21"
Margin="-2,-1.6,0,0"
SvgIcon="lucide/upload"
Opacity="0.4"
Theme="Black"
ToolTipService.InitialShowDelay="100"
ToolTipService.Placement="Right"
ToolTipService.VerticalOffset="-9"
Visibility="Collapsed" />
</Grid>
<!-- Tag 与 详情 -->
<StackPanel
x:Name="PanTags"
Grid.Row="2"
Grid.Column="3"
Margin="-1,0,1,0"
VerticalAlignment="Center"
IsHitTestVisible="False"
Orientation="Horizontal"
Visibility="Collapsed">
<!--<corelocal:BlurBorder Background="{DynamicResource ColorBrush6}" Padding="3,1" CornerRadius="3" Margin="0,0,4,0" SnapsToDevicePixels="True" UseLayoutRounding="False">
<TextBlock Text="科技" Foreground="{DynamicResource ColorBrush2}" FontSize="11" />
</corelocal:BlurBorder>-->
</StackPanel>
<TextBlock
x:Name="LabInfo"
Grid.Row="2"
Grid.Column="4"
Margin="0,0,3,1"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource ColorBrushGray4}"
IsHitTestVisible="False"
SnapsToDevicePixels="False"
TextTrimming="CharacterEllipsis"
UseLayoutRounding="False" />
</Grid>
@@ -0,0 +1,886 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.VisualBasic;
using PCL.Core.App;
using PCL.Core.Utils;
using PCL.Core.Utils.Exts;
using PCL.Core.App.Localization;
namespace PCL;
public partial class MyLocalCompItem
{
private string GetUpdateCompareDescription()
{
var currentName = Entry.compFile.FileName.Replace(".jar", "");
var newestName = Entry.UpdateFile.FileName.Replace(".jar", "");
// 简化名称对比
var currentSegs = currentName.Split('-').ToList();
var newestSegs = newestName.Split('-').ToList();
var shortened = false;
foreach (var Seg in currentSegs.ToList())
{
if (!newestSegs.Contains(Seg))
continue;
currentSegs.Remove(Seg);
newestSegs.Remove(Seg);
shortened = true;
}
if (shortened && currentSegs.Any() && newestSegs.Any())
{
currentName = currentSegs.Join("-");
newestName = newestSegs.Join("-");
Entry._Version = currentName; // 使用网络信息作为显示的版本号
}
return
Lang.Text("Instance.Resource.Item.UpdateCompare", currentName, Lang.TimeSpan(Entry.compFile.ReleaseDate - DateTime.Now), newestName, Lang.TimeSpan(Entry.UpdateFile.ReleaseDate - DateTime.Now));
}
public void Refresh()
{
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
// 更新
if (Entry.CanUpdate)
{
BtnUpdate.Visibility = Visibility.Visible;
BtnUpdate.ToolTip = $"{GetUpdateCompareDescription()}\r\n{Lang.Text("Instance.Resource.Item.UpdateToolTip")}";
}
else
{
BtnUpdate.Visibility = Visibility.Collapsed;
}
// 标题与描述
string descFileName;
if (Entry.IsFolder)
// 文件夹项的特殊处理
descFileName = Entry.Name;
else
switch (Entry.State)
{
case ModLocalComp.LocalCompFile.LocalFileStatus.Fine:
{
descFileName = ModBase.GetFileNameWithoutExtentionFromPath(Entry.path);
break;
}
case ModLocalComp.LocalCompFile.LocalFileStatus.Disabled:
{
descFileName =
ModBase.GetFileNameWithoutExtentionFromPath(Entry.path.Replace(".disabled", "")
.Replace(".old", "")); // McMod.McModState.Unavailable
break;
}
default:
{
descFileName = ModBase.GetFileNameFromPath(Entry.path);
break;
}
}
string newDescription;
var compTemp = Entry.Comp;
if (Entry.IsFolder)
{
// 文件夹项的特殊显示
Title = Entry.Name;
newDescription = Entry.Description;
}
else if (Config.Download.Comp.UiCompNameSolution == 1)
{
// 标题显示文件名,详情显示译名
// 标题
Title = descFileName;
SubTitle = "";
// 描述
if (Entry.Comp is null)
{
newDescription = Entry.Name;
}
else
{
var titles = await Task.Run(() => compTemp.GetControlTitle(false));
newDescription = titles.Key + titles.Value;
}
newDescription = newDescription.Replace(" | ", " / ");
if (Entry.Version is not null)
newDescription += $" ({Entry.Version})";
}
else
{
// 标题显示译名,详情显示文件名
// 标题
if (Entry.Comp is null)
{
Title = Entry.Name;
SubTitle = Entry.Version is null ? "" : " | " + Entry.Version;
}
else
{
var titles = await Task.Run(() => compTemp.GetControlTitle(false));
Title = titles.Key;
SubTitle = titles.Value + (Entry.Version is null ? "" : " | " + Entry.Version);
}
// 描述
newDescription = descFileName;
}
if (Entry.Comp is not null)
newDescription += ": " + Entry.Comp.Description.Replace("\r", "").Replace("\n", "");
else if (Entry.Description is not null)
newDescription += ": " + Entry.Description.Replace("\r", "").Replace("\n", "");
else if (!Entry.IsFileAvailable) newDescription += ": " + Lang.Text("Instance.Resource.Item.InfoUnavailable");
Description = newDescription;
if (Checked)
LabTitle.SetResourceReference(TextBlock.ForegroundProperty,
Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? "ColorBrush2" : "ColorBrush5");
else
LabTitle.SetResourceReference(TextBlock.ForegroundProperty,
Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? "ColorBrush1" : "ColorBrushGray4");
// 主 Logo
Logo = Entry.GetLogo();
// 图标右下角的 Logo
if (Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
{
if (imgState is not null)
{
Children.Remove(imgState);
imgState = null;
}
}
else
{
if (imgState is null)
{
imgState = new Image
{
Width = 20d,
Height = 20d,
Margin = new Thickness(0d, 0d, -5, -3),
IsHitTestVisible = false,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Bottom
};
RenderOptions.SetBitmapScalingMode(imgState, BitmapScalingMode.HighQuality);
SetColumn(imgState, 1);
SetRow(imgState, 1);
SetRowSpan(imgState, 2);
Children.Add(imgState);
// <Image x:Name="ImgState" RenderOptions.BitmapScalingMode="HighQuality" Width="16" Height="16" Margin="0,0,-3,-1"
// Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" IsHitTestVisible="False"
// HorizontalAlignment="Right" VerticalAlignment="Bottom"
// Source="/Images/Icons/Unavailable.png" />
}
imgState.Source = new MyBitmap(ModBase.pathImage + $"Icons/{Entry.State}.png");
}
// 标签
if (Entry.IsFolder)
// 为文件夹添加标签
Tags = new List<string> { Lang.Text("Instance.Resource.Item.FolderTag") };
else if (Entry.Comp is not null) Tags = Entry.Comp.Tags;
}));
}
public void RefreshColor(object sender, EventArgs e)
{
InitLate(sender, e);
// 触发颜色动画
var time = IsMouseOver ? 120 : 180;
var ani = new List<ModAnimation.AniData>();
// ButtonStack
if (buttonStack is not null)
{
if (IsMouseOver)
{
ani.Add(ModAnimation.AaOpacity(buttonStack, 1d - buttonStack.Opacity, (int)Math.Round(time * 0.7d),
(int)Math.Round(time * 0.3d)));
ani.Add(ModAnimation.AaDouble(
i => ColumnPaddingRight.Width =
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
5 + Buttons.Count() * 25 - ColumnPaddingRight.Width.Value, (int)Math.Round(time * 0.3d),
(int)Math.Round(time * 0.7d)));
}
else
{
ani.Add(ModAnimation.AaOpacity(buttonStack, -buttonStack.Opacity, (int)Math.Round(time * 0.4d)));
ani.Add(ModAnimation.AaDouble(
i => ColumnPaddingRight.Width =
new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)),
4d - ColumnPaddingRight.Width.Value, (int)Math.Round(time * 0.4d)));
}
}
// RectBack
if (IsMouseOver || Checked)
{
ani.AddRange(new[]
{
ModAnimation.AaColor(RectBack, Border.BackgroundProperty, isMouseDown ? "ColorBrush6" : "ColorBrushBg1",
time),
ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, time, ease: new ModAnimation.AniEaseOutFluent())
});
if (isMouseDown)
ani.Add(ModAnimation.AaScaleTransform(RectBack,
0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(time * 1.2d),
ease: new ModAnimation.AniEaseOutFluent()));
else
ani.Add(ModAnimation.AaScaleTransform(RectBack, 1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX,
(int)Math.Round(time * 1.2d), ease: new ModAnimation.AniEaseOutFluent()));
}
else
{
ani.AddRange(new[]
{
ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, time),
ModAnimation.AaScaleTransform(RectBack, 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX,
time, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaScaleTransform(RectBack, -0.196d, 1, after: true)
});
}
ModAnimation.AniStart(ani, "LocalModItem Color " + Uuid);
}
// 触发虚拟化内容
private void InitLate(object sender, EventArgs e)
{
if (buttonHandler is not null)
{
buttonHandler((MyLocalCompItem)sender, e);
buttonHandler = null;
}
}
// 显示更新日志
private void BtnUpdate_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
ShowUpdateLog();
}
private void ShowUpdateLog()
{
if (Entry.Comp is not null)
{
if (!Information.IsNumeric(Entry.Comp.Id))
{
var modrinthUrl = Entry.changelogUrls.FirstOrDefault(x => x.Contains("modrinth.com"));
if (modrinthUrl is not null)
{
ModBase.OpenWebsite(modrinthUrl);
return;
}
}
else
{
var curseForgeUrl = Entry.changelogUrls.FirstOrDefault(x => x.Contains("curseforge.com"));
if (curseForgeUrl is not null)
{
ModBase.OpenWebsite(curseForgeUrl);
return;
}
}
}
ModBase.Log(
Lang.Text("Instance.Resource.Item.OpenChangelogFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Resource.Item.OpenChangelogFailed"));
}
// 触发更新
private void BtnUpdate_Click(object sender, EventArgs e)
{
switch (ModMain.MyMsgBox(
$"{Lang.Text("Instance.Resource.Item.UpdateConfirm.Message", Entry.Name)}\r\n\r\n{GetUpdateCompareDescription()}",
Lang.Text("Instance.Resource.Item.UpdateConfirm.Title"),
Lang.Text("Instance.Resource.Item.Update"), Lang.Text("Instance.Resource.Item.ViewChangelog"), Lang.Text("Common.Action.Cancel")))
{
case 1: // 更新
{
switch (Entry.Comp.Type)
{
case ModComp.CompType.Mod:
{
ModMain.frmInstanceMod ??= new PageInstanceCompResource(ModComp.CompType.Mod);
ModMain.frmInstanceMod.UpdateResource(new[] { Entry });
break;
}
case ModComp.CompType.ResourcePack:
{
ModMain.frmInstanceResourcePack ??= new PageInstanceCompResource(ModComp.CompType.ResourcePack);
ModMain.frmInstanceResourcePack.UpdateResource(new[] { Entry });
break;
}
case ModComp.CompType.Shader:
{
ModMain.frmInstanceShader ??= new PageInstanceCompResource(ModComp.CompType.Shader);
ModMain.frmInstanceShader.UpdateResource(new[] { Entry });
break;
}
case ModComp.CompType.DataPack:
{
ModMain.frmInstanceSavesDatapack ??= new PageInstanceSavesDatapack();
ModMain.frmInstanceSavesDatapack.UpdateResource(new[] { Entry });
break;
}
}
break;
}
case 2: // 查看更新日志
{
ShowUpdateLog();
break;
}
case 3: // 取消
{
break;
}
}
}
// 自适应(#4465
private void PanTitle_SizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
// 0:全部舒展:Auto - Auto - (Auto) - 1*
// 1:压缩 SubtitleAuto - 1* - (Auto) - 0
// 2:继续压缩 Title1* - 0 - (Auto) - 0
var currentCompressLevel =
ColumnExtend.Width.IsStar ? 0 : ColumnTitle.Width.IsStar ? 2 : 1; // Subtitle 可能是 Collapsed
var newCompressLevel = default(int);
switch (currentCompressLevel)
{
case 0:
{
if (ColumnExtend.ActualWidth < 0.5d)
newCompressLevel = LabSubtitle.Visibility == Visibility.Collapsed ? 2 : 1;
else
return;
break;
}
case 1:
{
if (ColumnSubtitle.ActualWidth < 0.5d)
newCompressLevel = 2;
else if (!LabSubtitle.IsTextTrimmed())
newCompressLevel = 0;
else
return;
break;
}
case 2:
{
if (!LabTitle.IsTextTrimmed())
newCompressLevel = LabSubtitle.Visibility == Visibility.Collapsed ? 0 : 1;
else
return;
break;
}
}
switch (newCompressLevel)
{
case 0:
{
// 全部舒展:Auto - Auto - (Auto) - 1*
ColumnTitle.Width = GridLength.Auto;
ColumnSubtitle.Width = GridLength.Auto;
ColumnExtend.Width = new GridLength(1d, GridUnitType.Star);
break;
}
case 1:
{
// 压缩 SubtitleAuto - 1* - (Auto) - 0
ColumnTitle.Width = GridLength.Auto;
ColumnSubtitle.Width = new GridLength(1d, GridUnitType.Star);
ColumnExtend.Width = new GridLength(0d, GridUnitType.Pixel);
break;
}
case 2:
{
// 继续压缩 Title1* - 0 - (Auto) - 0
ColumnTitle.Width = new GridLength(1d, GridUnitType.Star);
ColumnSubtitle.Width = new GridLength(0d, GridUnitType.Pixel);
ColumnExtend.Width = new GridLength(0d, GridUnitType.Pixel);
break;
}
}
}
#region
public int Uuid = ModBase.GetUuid();
// Logo
public string Logo
{
get => PathLogo.Source;
set => PathLogo.Source = value;
}
// 标题
public string Title
{
get => field;
set
{
var rawValue = value;
switch (Entry.State)
{
case ModLocalComp.LocalCompFile.LocalFileStatus.Fine:
{
LabTitle.TextDecorations = null;
break;
}
case ModLocalComp.LocalCompFile.LocalFileStatus.Disabled:
{
LabTitle.TextDecorations = TextDecorations.Strikethrough;
break;
}
case ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable:
{
LabTitle.TextDecorations = TextDecorations.Strikethrough;
value += Lang.Text("Instance.Resource.Item.ErrorSuffix");
break;
}
}
if ((LabTitle.Text ?? "") == (value ?? ""))
return;
LabTitle.Text = value;
field = rawValue;
}
}
// 副标题
public string SubTitle
{
get => LabSubtitle?.Text ?? "";
set
{
if ((LabSubtitle.Text ?? "") == (value ?? ""))
return;
LabSubtitle.Text = value;
LabSubtitle.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible;
}
}
// 描述
public string Description
{
get => LabInfo.Text;
set
{
if ((LabInfo.Text ?? "") == (value ?? ""))
return;
LabInfo.Text = value;
}
}
// Tag
public List<string> Tags
{
set
{
PanTags.Children.Clear();
PanTags.Visibility = value.Any() ? Visibility.Visible : Visibility.Collapsed;
foreach (var TagText in value)
{
var newTag = new Border
{
Background = new SolidColorBrush(Color.FromArgb(12, 0, 0, 0)),
Padding = new Thickness(3d, 1d, 3d, 1d),
CornerRadius = new CornerRadius(3d),
Margin = new Thickness(0d, 0d, 3d, 0d),
SnapsToDevicePixels = true,
UseLayoutRounding = false
};
var tagTextBlock = new TextBlock
{
Text = TagText,
Foreground = new SolidColorBrush(ThemeManager.IsDarkMode
? Color.FromArgb(88, 255, 255, 255)
: Color.FromArgb(88, 136, 136, 136)),
FontSize = 11d
};
newTag.Child = tagTextBlock;
PanTags.Children.Add(newTag);
}
}
}
// 相关联的 Mod
public ModLocalComp.LocalCompFile Entry
{
get => (ModLocalComp.LocalCompFile)Tag;
set => Tag = value;
}
#endregion
#region
// 触发点击事件
public event ClickEventHandler? Click;
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
public MyLocalCompItem()
{
InitializeComponent();
PreviewMouseLeftButtonUp += Button_MouseUp;
PreviewMouseLeftButtonDown += Button_MouseDown;
MouseLeave += Button_MouseLeave;
PreviewMouseLeftButtonUp += Button_MouseLeave;
MouseLeftButtonDown += Button_MouseSwipeStart;
MouseEnter += Button_MouseSwipe;
MouseLeave += Button_MouseSwipe;
MouseLeftButtonUp += Button_MouseSwipe;
Loaded += (_, _) => Refresh();
MouseEnter += RefreshColor;
MouseLeave += RefreshColor;
MouseLeftButtonDown += RefreshColor;
MouseLeftButtonUp += RefreshColor;
Changed += RefreshColor;
// Handles
BtnUpdate.PreviewMouseRightButtonUp += BtnUpdate_PreviewMouseRightButtonUp;
BtnUpdate.Click += BtnUpdate_Click;
PanTitle.SizeChanged += PanTitle_SizeChanged;
}
private void Button_MouseUp(object sender, MouseButtonEventArgs e)
{
if (isMouseDown)
{
Click?.Invoke(sender, e);
if (e.Handled)
return;
ModBase.Log("[Control] 按下本地 Mod 列表项:" + LabTitle.Text);
}
}
// 鼠标点击判定
private bool isMouseDown;
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
if (!IsMouseDirectlyOver)
return;
isMouseDown = true;
if (buttonStack is not null)
buttonStack.IsHitTestVisible = false;
}
private void Button_MouseLeave(object sender, object e)
{
isMouseDown = false;
if (buttonStack is not null)
buttonStack.IsHitTestVisible = true;
}
// 滑动选中
public class SwipeSelect
{
public int Start { get; set; }
public int End { get; set; }
public bool Swiping
{
get => field;
set
{
field = value;
if (TargetFrm is not null)
try
{
var cardSelect = Interaction.CallByName(TargetFrm, "CardSelect", CallType.Get);
Interaction.CallByName(cardSelect, "IsHitTestVisible", CallType.Set, !value);
}
catch
{
}
}
}
public bool SwipeToState { get; set; }
public object TargetFrm { get; set; }
}
public SwipeSelect CurrentSwipe { get; set; }
private void Button_MouseSwipeStart(object sender, object e)
{
if (Parent is null)
return; // Mod 可能已被删除(#3824
// 开始滑动
var index = ((StackPanel)Parent).Children.IndexOf(this);
CurrentSwipe.Start = index;
CurrentSwipe.End = index;
CurrentSwipe.Swiping = true;
CurrentSwipe.SwipeToState = !Checked;
}
private void Button_MouseSwipe(object sender, object e)
{
if (Parent is null)
return; // Mod 可能已被删除(#3824
// 结束滑动
if (Mouse.LeftButton != MouseButtonState.Pressed || !(Mouse.DirectlyOver is MyLocalCompItem)) // #5771
{
CurrentSwipe.Swiping = false;
return;
}
// 计算滑动范围
var elements = ((StackPanel)Parent).Children;
var index = elements.IndexOf(this);
CurrentSwipe.Start =
(int)Math.Round(ModBase.MathClamp(Math.Min(CurrentSwipe.Start, index), 0d, elements.Count - 1));
CurrentSwipe.End =
(int)Math.Round(ModBase.MathClamp(Math.Max(CurrentSwipe.End, index), 0d, elements.Count - 1));
// 勾选所有范围中的项
if (CurrentSwipe.Start == CurrentSwipe.End)
return;
for (int i = CurrentSwipe.Start, loopTo = CurrentSwipe.End; i <= loopTo; i++)
{
var item = (MyLocalCompItem)elements[i];
item.InitLate(item, (EventArgs)e);
item.Checked = CurrentSwipe.SwipeToState;
}
}
// 勾选状态
public event CheckEventHandler? Check;
public delegate void CheckEventHandler(object sender, ModBase.RouteEventArgs e);
public event ChangedEventHandler? Changed;
public delegate void ChangedEventHandler(object sender, ModBase.RouteEventArgs e);
public bool Checked
{
get => field;
set
{
try
{
// 触发属性值修改
var rawValue = field;
if (value == field)
return;
field = value;
var ChangedEventArgs = new ModBase.RouteEventArgs();
if (IsInitialized)
{
Changed?.Invoke(this, ChangedEventArgs);
if (ChangedEventArgs.handled)
{
field = rawValue;
return;
}
}
if (value)
{
var checkEventArgs = new ModBase.RouteEventArgs();
Check?.Invoke(this, checkEventArgs);
if (checkEventArgs.handled)
return;
}
// 更改动画
if (this.IsVisibleInWindow(ModMain.frmMain))
{
var anim = new List<ModAnimation.AniData>();
if (Checked)
{
// 由无变有
var delta = 32d - RectCheck.ActualHeight;
anim.Add(ModAnimation.AaHeight(RectCheck, delta * 0.4d, 200,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
anim.Add(ModAnimation.AaHeight(RectCheck, delta * 0.6d, 300,
ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)));
anim.Add(ModAnimation.AaOpacity(RectCheck, 1d - RectCheck.Opacity, 30));
RectCheck.VerticalAlignment = VerticalAlignment.Center;
RectCheck.Margin = new Thickness(-3, 0d, 0d, 0d);
anim.Add(ModAnimation.AaColor(LabTitle, TextBlock.ForegroundProperty,
Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine
? "ColorBrush2"
: "ColorBrush5", 200));
}
else
{
// 由有变无
anim.Add(ModAnimation.AaHeight(RectCheck, -RectCheck.ActualHeight, 120,
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)));
anim.Add(ModAnimation.AaOpacity(RectCheck, -RectCheck.Opacity, 70, 40));
RectCheck.VerticalAlignment = VerticalAlignment.Center;
anim.Add(ModAnimation.AaColor(LabTitle, TextBlock.ForegroundProperty,
LabTitle.TextDecorations is null ? "ColorBrush1" : "ColorBrushGray4", 120));
}
ModAnimation.AniStart(anim, "MyLocalCompItem Checked " + Uuid);
}
else
{
// 不在窗口上时直接设置
RectCheck.VerticalAlignment = VerticalAlignment.Center;
RectCheck.Margin = new Thickness(-3, 0d, 0d, 0d);
if (Checked)
{
RectCheck.Height = 32d;
RectCheck.Opacity = 1d;
LabTitle.SetResourceReference(TextBlock.ForegroundProperty,
Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine
? "ColorBrush2"
: "ColorBrush5");
}
else
{
RectCheck.Height = 0d;
RectCheck.Opacity = 0d;
LabTitle.SetResourceReference(TextBlock.ForegroundProperty,
Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine
? "ColorBrush1"
: "ColorBrushGray4");
}
ModAnimation.AniStop("MyLocalCompItem Checked " + Uuid);
}
}
catch (Exception ex)
{
ModBase.Log(ex, "设置 Checked 失败");
}
}
}
#endregion
#region
// 右下角状态指示图标
private Image imgState;
// 指向背景
public Border RectBack
{
get
{
if (field is null)
{
var rect = new Border
{
Name = "RectBack",
CornerRadius = new CornerRadius(3d),
RenderTransform = new ScaleTransform(0.8d, 0.8d),
RenderTransformOrigin = new Point(0.5d, 0.5d),
BorderThickness = new Thickness(ModBase.GetWPFSize(1d)),
SnapsToDevicePixels = true,
IsHitTestVisible = false,
Opacity = 0d
};
rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7");
rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6");
SetColumnSpan(rect, 999);
SetRowSpan(rect, 999);
Children.Insert(0, rect);
field = rect;
// <!--<corelocal:BlurBorder x:Name = "RectBack" CornerRadius="3" RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True"
// IsHitTestVisible = "False" Opacity="0" BorderThickness="1"
// Grid.ColumnSpan = "4" Background="{DynamicResource ColorBrush7}" BorderBrush="{DynamicResource ColorBrush6}"/>-->
}
return field;
}
}
// 按钮
public Action<MyLocalCompItem, EventArgs> buttonHandler;
public FrameworkElement buttonStack;
public IEnumerable<MyIconButton> Buttons
{
get => field;
set
{
field = value;
// 移除原 Stack
if (buttonStack is not null)
{
Children.Remove(buttonStack);
buttonStack = null;
}
if (!value.Any())
return;
// 添加新 Stack
buttonStack = new StackPanel
{
Opacity = 0d,
Margin = new Thickness(0d, 0d, 5d, 0d),
SnapsToDevicePixels = false,
Orientation = (Orientation)System.Windows.Forms.Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
UseLayoutRounding = false
};
SetColumnSpan(buttonStack, 10);
SetRowSpan(buttonStack, 10);
// 构造按钮
foreach (var Btn in value)
{
if (Btn.Height.Equals(double.NaN))
Btn.Height = 25d;
if (Btn.Width.Equals(double.NaN))
Btn.Width = 25d;
((StackPanel)buttonStack).Children.Add(Btn);
}
Children.Add(buttonStack);
}
}
// 勾选条
public Border RectCheck
{
get
{
if (field is null)
{
field = new Border
{
Width = 5d,
Height = Checked ? double.NaN : 0d,
CornerRadius = new CornerRadius(2d, 2d, 2d, 2d),
VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Left,
UseLayoutRounding = false,
SnapsToDevicePixels = false,
Margin = Checked ? new Thickness(-3, 6d, 0d, 6d) : new Thickness(-3, 0d, 0d, 0d)
};
field.SetResourceReference(Border.BackgroundProperty, "ColorBrush3");
SetRowSpan(field, 10);
Children.Add(field);
}
return field;
}
}
#endregion
}
@@ -0,0 +1,137 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceCompResource"
PanScroll="{Binding ElementName=PanBack}">
<Grid>
<Grid x:Name="PanAllBack">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
x:Name="PanBack">
<StackPanel x:Name="PanMain" Margin="25,10,25,10" Grid.IsSharedSizeScope="True"> <!-- 若显示多选模块则为 92 -->
<local:MySearchBox Margin="0,15" HintText="{DynamicResource Instance.Resource.Search.Hint}" x:Name="SearchBox" />
<local:MyCard Margin="0,0,0,15" x:Name="PanManage">
<WrapPanel Margin="15,8,0,15">
<local:MyButton x:Name="BtnManageBack" MinWidth="110" Text="{DynamicResource Instance.Resource.BackToParent}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" Visibility="Collapsed" />
<local:MyButton x:Name="BtnManageOpen" MinWidth="110" Text="{DynamicResource Common.Action.OpenFolder}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" ColorType="Highlight" />
<local:MyButton x:Name="BtnManageInstall" MinWidth="110" Text="{DynamicResource Instance.Resource.InstallFromFiles}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageDownload" MinWidth="110" Text="{DynamicResource Instance.Resource.DownloadNew}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageSelectAll" MinWidth="110" Text="{DynamicResource Instance.Resource.SelectAll}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageInfoExport" MinWidth="110" Text="{DynamicResource Instance.Resource.ExportInfo}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" ToolTip="{DynamicResource Instance.Resource.ExportInfo.ToolTip}" />
<local:MyButton x:Name="BtnManageCheck" MinWidth="110" Text="{DynamicResource Instance.Resource.CheckMods}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left"
ToolTip="{DynamicResource Instance.Resource.CheckMods.ToolTip}"
Visibility="Collapsed" />
</WrapPanel>
</local:MyCard>
<local:MyCard x:Name="PanListBack" VerticalAlignment="Top" Opacity="0" Margin="0,0,0,14" Title=" "
MinHeight="55">
<StackPanel x:Name="PanFilter" Margin="15,13,0,0" Orientation="Horizontal"
VerticalAlignment="Top" HorizontalAlignment="Left" Height="28">
<local:MyRadioButton Tag="0" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.All}" x:Name="BtnFilterAll" Checked="True" />
<local:MyRadioButton Tag="1" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Enabled}" x:Name="BtnFilterEnabled" />
<local:MyRadioButton Tag="2" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Disabled}" x:Name="BtnFilterDisabled" />
<local:MyRadioButton Tag="3" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Updatable}" x:Name="BtnFilterCanUpdate" />
<local:MyRadioButton Tag="4" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Error}" x:Name="BtnFilterError" />
<local:MyRadioButton Tag="5" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Duplicate}" x:Name="BtnFilterDuplicate" />
</StackPanel>
<StackPanel Margin="0,13,15,0" Orientation="Horizontal" VerticalAlignment="Top"
HorizontalAlignment="Right" Height="28">
<local:MyIconTextButton x:Name="BtnSort" Text="{DynamicResource Instance.Resource.Sort.Text}"
SvgIcon="lucide/arrow-up-down" />
</StackPanel>
<StackPanel Margin="20,48,18,22" Name="PanList" VerticalAlignment="Top" />
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanEmpty">
<StackPanel Margin="20,17">
<TextBlock x:Name="TxtEmptyTitle" Margin="0,0,0,9" HorizontalAlignment="Center" Text="{DynamicResource Instance.Resource.Empty.Title}"
FontSize="19" UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle HorizontalAlignment="Stretch" Height="2" Fill="{DynamicResource ColorBrush3}" />
<TextBlock x:Name="TxtEmptyDescription" Margin="10,10,10,0"
Text="{DynamicResource Instance.Resource.Empty.Description}" TextWrapping="Wrap" />
<WrapPanel Margin="10,20,10,5" HorizontalAlignment="Center" Orientation="Horizontal">
<local:MyButton Height="35" x:Name="BtnHintBack" MinWidth="130" Text="{DynamicResource Instance.Resource.BackToParent}" Margin="5,0"
Padding="12,0" Visibility="Collapsed" />
<local:MyButton Height="35" x:Name="BtnHintInstall" MinWidth="130" Text="{DynamicResource Instance.Resource.InstallFromFiles}" Margin="5,0"
Padding="12,0" ColorType="Highlight" />
<local:MyButton Height="35" x:Name="BtnHintDownload" MinWidth="130" Text="{DynamicResource Instance.Resource.DownloadNew}" Margin="5,0"
Padding="12,0" />
<local:MyButton Height="35" x:Name="BtnHintOpen" MinWidth="130" Text="{DynamicResource Common.Action.OpenFolder}" Margin="5,0"
Padding="12,0" />
</WrapPanel>
</StackPanel>
</local:MyCard>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40"
x:Name="PanSchematicEmpty" Visibility="Collapsed">
<StackPanel Margin="20,17">
<TextBlock Margin="0,0,0,9" HorizontalAlignment="Center" Text="{DynamicResource Instance.Resource.Schematic.Unavailable.Title}" FontSize="19"
UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle HorizontalAlignment="Stretch" Height="2" Fill="{DynamicResource ColorBrush3}" />
<TextBlock Margin="10,10,10,0"
Text="{DynamicResource Instance.Resource.Schematic.Unavailable.Message}"
TextWrapping="Wrap" HorizontalAlignment="Center" />
<WrapPanel Margin="10,20,10,5" HorizontalAlignment="Center" Orientation="Horizontal">
<local:MyButton Height="35" x:Name="BtnSchematicDownloadMod" MinWidth="130" Text="{DynamicResource Instance.Resource.Schematic.DownloadMod}"
Margin="5,0" Padding="12,0" ColorType="Highlight" />
<local:MyButton Height="35" x:Name="BtnSchematicVersionSelect" MinWidth="130" Text="{DynamicResource Instance.Resource.Mod.Disabled.SelectInstance}"
Margin="5,0" Padding="12,0" />
</WrapPanel>
</StackPanel>
</local:MyCard>
</Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True"
x:Name="PanLoad" UseAnimation="False">
<local:MyLoading Text="{DynamicResource Instance.Resource.Loading}" Margin="20,20,20,17" x:Name="Load" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</local:MyCard>
<local:MyCard x:Name="CardSelect" Visibility="Collapsed" Opacity="0"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="25,25,25,0" UseAnimation="False">
<local:MyCard.RenderTransform>
<TranslateTransform x:Name="TransSelect" Y="-10" />
</local:MyCard.RenderTransform>
<TextBlock x:Name="LabSelect" Text="" HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="9" Foreground="{DynamicResource ColorBrush2}" />
<StackPanel Orientation="Horizontal" Margin="5,28,5,5">
<local:MyIconTextButton x:Name="BtnSelectUpdate" Text="{DynamicResource Instance.Resource.Update}"
LogoScale="1"
SvgIcon="lucide/upload" />
<local:MyIconTextButton x:Name="BtnSelectEnable" Text="{DynamicResource Instance.Resource.Enable}"
LogoScale="1.05"
SvgIcon="lucide/circle-check" />
<local:MyIconTextButton x:Name="BtnSelectDisable" Text="{DynamicResource Instance.Resource.Disable}"
LogoScale="1"
SvgIcon="lucide/circle-minus" />
<local:MyIconTextButton x:Name="BtnSelectFavorites" Text="{DynamicResource Instance.Resource.Favorite}"
LogoScale="1"
SvgIcon="lucide/heart" />
<local:MyIconTextButton x:Name="BtnSelectShare" Text="{DynamicResource Instance.Resource.ShareSelected}"
LogoScale="1"
SvgIcon="lucide/share-2" />
<local:MyIconTextButton x:Name="BtnSelectDelete" Text="{DynamicResource Common.Action.Delete}"
LogoScale="0.96"
SvgIcon="lucide/trash-2" />
<local:MyIconTextButton x:Name="BtnSelectCancel" Text="{DynamicResource Instance.Resource.CancelSelection}"
LogoScale="0.8"
SvgIcon="lucide/x" />
</StackPanel>
</local:MyCard>
</Grid>
</local:MyPageRight>
@@ -0,0 +1,2961 @@
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App;
using PCL.Core.Logging;
using PCL.Core.UI;
using PCL.Core.UI.Theme;
using PCL.Network;
using PCL.Network.Loaders;
using FileSystem = Microsoft.VisualBasic.FileSystem;
using SearchOption = System.IO.SearchOption;
using PCL.Core.App.Localization;
using PCL.Core.Utils;
namespace PCL;
public partial class PageInstanceCompResource : IRefreshable
{
#region
// 模组信息缓存 - 解决排序时重复创建FileInfo导致的性能问题
private readonly Dictionary<string, (DateTime CreationTime, long Length)> modFileInfoCache = new();
public PageInstanceCompResource()
{
InitializeComponent();
Unloaded += Page_Unloaded;
Loaded += (_, _) => PageOther_Loaded();
Initialized += (_, _) => LoaderInit();
PageExit += UnselectedAllWithAnimation;
Load.Click += Load_Click;
BtnManageBack.Click += BtnManageBack_Click;
BtnHintBack.Click += BtnHintBack_Click;
BtnManageOpen.Click += BtnManageOpen_Click;
BtnHintOpen.Click += BtnManageOpen_Click;
BtnManageSelectAll.Click += BtnManageSelectAll_Click;
BtnManageInstall.Click += BtnManageInstall_Click;
BtnHintInstall.Click += BtnManageInstall_Click;
BtnManageInfoExport.Click += BtnManageInfoExport_Click;
BtnManageDownload.Click += BtnManageDownload_Click;
BtnHintDownload.Click += BtnManageDownload_Click;
BtnSchematicDownloadMod.Click += BtnSchematicDownloadMod_Click;
BtnSchematicVersionSelect.Click += BtnSchematicVersionSelect_Click;
Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation();
SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown;
BtnFilterAll.Check += ChangeFilter;
BtnFilterCanUpdate.Check += ChangeFilter;
BtnFilterDisabled.Check += ChangeFilter;
BtnFilterEnabled.Check += ChangeFilter;
BtnFilterError.Check += ChangeFilter;
BtnFilterDuplicate.Check += ChangeFilter;
BtnSort.Click += BtnSortClick;
BtnSelectEnable.Click += BtnSelectED_Click;
BtnSelectDisable.Click += BtnSelectED_Click;
BtnSelectUpdate.Click += BtnSelectUpdate_Click;
BtnSelectDelete.Click += BtnSelectDelete_Click;
BtnSelectCancel.Click += BtnSelectCancel_Click;
BtnSelectFavorites.Click += BtnSelectFavorites_Click;
BtnSelectShare.Click += BtnSelectShare_Click;
SearchBox.TextChanged += SearchRun;
}
// 获取模组信息(带缓存)
private (DateTime CreationTime, long Length) GetModFileInfo(string path)
{
(DateTime CreationTime, long Length) cacheItem;
if (modFileInfoCache.TryGetValue(path, out cacheItem)) return cacheItem;
try
{
var fileInfo = new FileInfo(path);
var newItem = (fileInfo.CreationTime, fileInfo.Length);
if (!modFileInfoCache.ContainsKey(path)) modFileInfoCache.Add(path, newItem);
return newItem;
}
catch (Exception ex)
{
ModBase.Log(ex, "获取模组信息失败: " + path);
return (DateTime.MinValue, 0L);
}
}
// 页面关闭时清理缓存
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
modFileInfoCache.Clear();
}
#endregion
#region
private readonly ModComp.CompType currentCompType = ModComp.CompType.Mod;
private readonly MyLocalCompItem.SwipeSelect currentSwipSelect;
public PageInstanceCompResource(ModComp.CompType loadCompType)
{
currentCompType = loadCompType;
CurrentFolderPath = ""; // 确保文件夹路径被重置为根目录
currentSwipSelect = new MyLocalCompItem.SwipeSelect { TargetFrm = this };
// 此调用是设计器所必需的。
InitializeComponent();
// 在 InitializeComponent() 调用之后添加任何初始化。
if (new[] { ModComp.CompType.Shader, ModComp.CompType.ResourcePack, ModComp.CompType.Schematic }.Contains(
currentCompType))
{
BtnSelectEnable.Visibility = Visibility.Collapsed;
BtnSelectDisable.Visibility = Visibility.Collapsed;
}
// 投影文件管理页隐藏下载按钮
if (currentCompType == ModComp.CompType.Schematic)
{
BtnManageDownload.Visibility = Visibility.Collapsed;
BtnHintDownload.Visibility = Visibility.Collapsed;
}
Unloaded += Page_Unloaded;
Loaded += (_, _) => PageOther_Loaded();
LoaderInit();
PageExit += UnselectedAllWithAnimation;
// Handles
Load.Click += Load_Click;
BtnManageBack.Click += BtnManageBack_Click;
BtnHintBack.Click += BtnHintBack_Click;
BtnManageOpen.Click += BtnManageOpen_Click;
BtnHintOpen.Click += BtnManageOpen_Click;
BtnManageSelectAll.Click += BtnManageSelectAll_Click;
BtnManageInstall.Click += BtnManageInstall_Click;
BtnHintInstall.Click += BtnManageInstall_Click;
BtnManageDownload.Click += BtnManageDownload_Click;
BtnHintDownload.Click += BtnManageDownload_Click;
BtnManageInfoExport.Click += BtnManageInfoExport_Click;
BtnSchematicDownloadMod.Click += BtnSchematicDownloadMod_Click;
BtnSchematicVersionSelect.Click += BtnSchematicVersionSelect_Click;
Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation();
SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown;
BtnFilterAll.Check += ChangeFilter;
BtnFilterCanUpdate.Check += ChangeFilter;
BtnFilterDisabled.Check += ChangeFilter;
BtnFilterEnabled.Check += ChangeFilter;
BtnFilterError.Check += ChangeFilter;
BtnFilterDuplicate.Check += ChangeFilter;
BtnSort.Click += BtnSortClick;
BtnSelectEnable.Click += BtnSelectED_Click;
BtnSelectDisable.Click += BtnSelectED_Click;
BtnSelectUpdate.Click += BtnSelectUpdate_Click;
BtnSelectDelete.Click += BtnSelectDelete_Click;
BtnSelectCancel.Click += BtnSelectCancel_Click;
BtnSelectFavorites.Click += BtnSelectFavorites_Click;
BtnSelectShare.Click += BtnSelectShare_Click;
SearchBox.TextChanged += SearchRun;
}
private ModLocalComp.CompLocalLoaderData GetRequireLoaderData()
{
var res = new ModLocalComp.CompLocalLoaderData();
res.gameVersion = PageInstanceLeft.McInstance;
res.frm = this;
var requireLoaders = new List<ModComp.CompLoaderType>();
switch (currentCompType)
{
case ModComp.CompType.Mod:
{
requireLoaders = ModLocalComp.GetCurrentVersionModLoader();
break;
}
case ModComp.CompType.ResourcePack:
{
requireLoaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList();
break;
}
case ModComp.CompType.Shader:
{
requireLoaders = new[]
{
ModComp.CompLoaderType.OptiFine, ModComp.CompLoaderType.Iris, ModComp.CompLoaderType.Vanilla,
ModComp.CompLoaderType.Canvas
}.ToList();
break;
}
case ModComp.CompType.Schematic:
{
requireLoaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList();
break;
}
}
res.loaders = requireLoaders;
res.compPath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
res.compType = currentCompType;
return res;
}
private bool isLoad;
public void PageOther_Loaded()
{
CurrentFolderPath = string.Empty;
if (ModMain.frmMain.pageLast.page != FormMain.PageType.CompDetail)
PanBack.ScrollToHome();
ModAnimation.AniControlEnabled += 1;
selectedMods.Clear();
ReloadCompFileList();
ChangeAllSelected(false);
ModAnimation.AniControlEnabled -= 1;
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
// 检查是否为原理图管理界面且首次打开
if (currentCompType == ModComp.CompType.Schematic && !States.Hint.SchematicFirstTime)
// 显示首次打开提示
ModBase.RunInUi(() =>
{
ModMain.MyMsgBox(Lang.Text("Instance.Saves.Folder.DoubleClickHint.Message"), Lang.Text("Instance.Saves.Folder.DoubleClickHint.Title"), Lang.Text("Common.Action.GotIt"));
States.Hint.SchematicFirstTime = true;
}, true);
ModMain.frmMain.KeyDown += FrmMain_KeyDown;
// 调整按钮边距(这玩意儿没法从 XAML 改)
foreach (MyRadioButton Btn in PanFilter.Children)
Btn.LabText.Margin = new Thickness(-2, 0d, 8d, 0d);
}
/// <summary>
/// 刷新 Mod 列表。
/// </summary>
public void ReloadCompFileList(bool forceReload = false)
{
if (LoaderRun(forceReload
? ModLoader.LoaderFolderRunType.ForceRun
: ModLoader.LoaderFolderRunType.RunOnUpdated))
{
ModBase.Log($"[System] 已刷新 {currentCompType} 列表");
modFileInfoCache.Clear();
ModBase.RunInUi(() =>
{
Filter = FilterType.All;
PanBack.ScrollToHome();
SearchBox.Text = "";
});
}
}
// 强制刷新
private void RefreshSelf()
{
Refresh(currentCompType);
}
void IRefreshable.Refresh()
{
RefreshSelf();
}
public static void Refresh(ModComp.CompType whichPage)
{
// 强制刷新
try
{
ModComp.compProjectCache.Clear();
ModComp.compFilesCache.Clear();
File.Delete(ModBase.pathTemp + @"Cache\LocalComp.json");
ModBase.Log("[CompResource] 由于点击刷新按钮,清理本地工程信息缓存");
}
catch (Exception ex)
{
ModBase.Log(ex, "强制刷新时清理本地工程信息缓存失败");
}
switch (whichPage)
{
case ModComp.CompType.Mod:
{
if (ModMain.frmInstanceMod is not null)
ModMain.frmInstanceMod.ReloadCompFileList(true); // 无需 Else,还没加载刷个鬼的新
ModMain.frmInstanceLeft.ItemMod.Checked = true;
break;
}
case ModComp.CompType.ResourcePack:
{
if (ModMain.frmInstanceResourcePack is not null)
ModMain.frmInstanceResourcePack.ReloadCompFileList(true);
ModMain.frmInstanceLeft.ItemResourcePack.Checked = true;
break;
}
case ModComp.CompType.Shader:
{
if (ModMain.frmInstanceShader is not null)
ModMain.frmInstanceShader.ReloadCompFileList(true);
ModMain.frmInstanceLeft.ItemShader.Checked = true;
break;
}
case ModComp.CompType.Schematic:
{
if (ModMain.frmInstanceSchematic is not null)
ModMain.frmInstanceSchematic.ReloadCompFileList(true);
ModMain.frmInstanceLeft.ItemSchematic.Checked = true;
break;
}
}
HintService.Hint(Lang.Text("Instance.Left.Refreshing"), log: false);
}
private void LoaderInit()
{
PageLoaderInit(Load, PanLoad, PanAllBack, null, ModLocalComp.compResourceListLoader,
_ => LoadUIFromLoaderOutput(), () => currentCompType, false);
}
private void Load_Click(object sender, MouseButtonEventArgs e)
{
if (ModLocalComp.compResourceListLoader.State == ModBase.LoadState.Failed)
LoaderRun(ModLoader.LoaderFolderRunType.ForceRun);
}
public bool LoaderRun(ModLoader.LoaderFolderRunType type)
{
string loadPath;
if (string.IsNullOrEmpty(CurrentFolderPath))
// 加载根目录
loadPath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
else
// 加载当前文件夹
loadPath = CurrentFolderPath;
return ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader, loadPath, type,
loaderInput: GetRequireLoaderData());
}
#endregion
#region
/// <summary>
/// 当前显示的文件夹路径。空字符串表示根目录。
/// </summary>
public string CurrentFolderPath { get; set; } = "";
/// <summary>
/// 进入指定的文件夹。
/// </summary>
private void EnterFolder(string folderPath)
{
try
{
if (string.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath))
{
HintService.Hint(Lang.Text("Instance.Saves.Folder.NotFound"), HintType.Error);
return;
}
CurrentFolderPath = folderPath;
ModBase.Log($"[原理图] 进入文件夹:{folderPath}");
ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader, folderPath,
ModLoader.LoaderFolderRunType.ForceRun, loaderInput: GetRequireLoaderData());
}
catch (Exception ex)
{
ModBase.Log(
ex,
"进入文件夹失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
/// <summary>
/// 进入指定文件夹。
/// </summary>
private void EnterFolderWithCheck(string folderPath)
{
try
{
EnterFolder(folderPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"进入文件夹失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
/// <summary>
/// 返回上级文件夹。
/// </summary>
private void GoBackToParentFolder()
{
if (string.IsNullOrEmpty(CurrentFolderPath))
return;
try
{
// 获取根路径
var rootPath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
rootPath = Path.GetFullPath(rootPath.TrimEnd('\\'));
// 获取父级路径
var parentPath = Directory.GetParent(CurrentFolderPath)?.FullName;
// 如果父级路径就是根路径或者父级路径不在根路径范围内,则返回根目录
if (parentPath is null || parentPath.Equals(rootPath, StringComparison.OrdinalIgnoreCase) ||
!parentPath.StartsWith(rootPath + @"\", StringComparison.OrdinalIgnoreCase))
CurrentFolderPath = "";
else
CurrentFolderPath = parentPath;
}
catch (Exception ex)
{
ModBase.Log(ex, "路径处理失败");
// 发生错误时直接返回根目录
CurrentFolderPath = "";
}
ModBase.Log($"[原理图] 返回上级文件夹:{(string.IsNullOrEmpty(CurrentFolderPath) ? "" : CurrentFolderPath)}");
// 重新加载当前文件夹的内容
string loadPath;
if (string.IsNullOrEmpty(CurrentFolderPath))
// 返回到根目录
loadPath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
else
// 加载当前文件夹
loadPath = CurrentFolderPath;
// 强制刷新UI状态
// 确保按钮状态正确
ModBase.RunInUi(() =>
BtnManageBack.Visibility =
!string.IsNullOrEmpty(CurrentFolderPath) ? Visibility.Visible : Visibility.Collapsed);
// 延迟一帧后再加载,确保UI状态已更新
ModBase.RunInUi(
() => ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader, loadPath,
ModLoader.LoaderFolderRunType.ForceRun, loaderInput: GetRequireLoaderData()), true);
}
#endregion
#region UI
/// <summary>
/// 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawPath。
/// </summary>
public Dictionary<string, MyLocalCompItem> modItems = new();
/// <summary>
/// 将加载器结果的 Mod 列表加载为 UI。
/// </summary>
private void LoadUIFromLoaderOutput()
{
try
{
// 判断应该显示哪一个页面
if (ModLocalComp.compResourceListLoader.output.Any())
{
PanBack.Visibility = Visibility.Visible;
PanEmpty.Visibility = Visibility.Collapsed;
PanSchematicEmpty.Visibility = Visibility.Collapsed;
}
else
{
// 检查是否为投影文件类型且schematics文件夹不存在
if (currentCompType == ModComp.CompType.Schematic)
{
var schematicsPath = PageInstanceLeft.McInstance.PathIndie + @"schematics\";
if (!Directory.Exists(schematicsPath))
{
PanSchematicEmpty.Visibility = Visibility.Visible;
PanEmpty.Visibility = Visibility.Collapsed;
PanBack.Visibility = Visibility.Collapsed;
return;
}
}
// 根据组件类型设置PanEmpty的文本内容
if (currentCompType == ModComp.CompType.Schematic)
{
// 检查是否在子文件夹中
if (!string.IsNullOrEmpty(CurrentFolderPath))
{
// 子文件夹为空的提示
TxtEmptyTitle.Text = Lang.Text("Instance.Resource.EmptyFolder.Title");
TxtEmptyDescription.Text = Lang.Text("Instance.Resource.EmptyFolder.Description");
}
else
{
// 根目录为空的提示
TxtEmptyTitle.Text = Lang.Text("Instance.Resource.Empty.Title");
TxtEmptyDescription.Text = Lang.Text("Instance.Resource.Empty.Description");
}
}
else
{
TxtEmptyTitle.Text = Lang.Text("Instance.Resource.Empty.Title");
TxtEmptyDescription.Text = Lang.Text("Instance.Resource.Empty.DescriptionWithDownload");
}
// 如果当前在子文件夹中,显示返回上一级按钮
if (!string.IsNullOrEmpty(CurrentFolderPath))
BtnHintBack.Visibility = Visibility.Visible;
else
BtnHintBack.Visibility = Visibility.Collapsed;
PanEmpty.Visibility = Visibility.Visible;
PanBack.Visibility = Visibility.Collapsed;
PanSchematicEmpty.Visibility = Visibility.Collapsed;
return;
}
// 修改缓存
modItems.Clear();
var rootPath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
rootPath = Path.GetFullPath(rootPath.TrimEnd('\\'));
var itemsToShow = ModLocalComp.compResourceListLoader.output.Where(item =>
{
var itemPath = item.IsFolder ? item.ActualPath : item.path;
var parentDir = Directory.GetParent(itemPath)?.FullName;
if (string.IsNullOrEmpty(CurrentFolderPath))
return parentDir.Equals(rootPath, StringComparison.OrdinalIgnoreCase);
return parentDir.Equals(CurrentFolderPath, StringComparison.OrdinalIgnoreCase);
}).ToList();
foreach (var ModEntity in itemsToShow)
modItems[ModEntity.RawPath] = BuildLocalCompItem(ModEntity);
// 显示结果
ModBase.RunInUi(() =>
{
Filter = FilterType.All;
SearchBox.Text = ""; // 这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
RefreshUI();
SetSortMethod(SortMethod.CompName);
});
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"加载 {currentCompType} 列表 UI 失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
private MyLocalCompItem BuildLocalCompItem(ModLocalComp.LocalCompFile entry)
{
try
{
ModAnimation.AniControlEnabled += 1;
var newItem = new MyLocalCompItem
{
SnapsToDevicePixels = true,
Entry = entry,
buttonHandler = BuildLocalCompItemBtnHandler,
Checked = selectedMods.Contains(entry.RawPath)
};
newItem.CurrentSwipe = currentSwipSelect;
newItem.Tags = entry.Tags;
entry.OnCompUpdate += _ => newItem.Refresh();
// AddHandler Entry.OnCompUpdate, Sub() RunInUi(Sub() DoSort())
newItem.Refresh();
ModAnimation.AniControlEnabled -= 1;
return newItem;
}
catch (Exception ex)
{
ModAnimation.AniControlEnabled -= 1;
ModBase.Log(ex, $"创建 UI 项失败:{entry.RawPath}");
throw;
}
}
private void BuildLocalCompItemBtnHandler(MyLocalCompItem sender, EventArgs e)
{
// 点击事件
sender.Changed += (ss, ee) => CheckChanged((MyLocalCompItem)ss, ee);
if (sender.Entry.IsFolder)
{
// 文件夹项的点击事件:双击进入文件夹,单击切换选中状态
var lastClickTime = DateTime.MinValue;
sender.Click += (sss, _) =>
{
var ss = (MyLocalCompItem)sss;
var currentTime = DateTime.Now;
var timeDiff = (currentTime - lastClickTime).TotalMilliseconds;
if (timeDiff <= 300d)
// 300ms内双击,进入文件夹
EnterFolderWithCheck(ss.Entry.ActualPath);
else
// 单击切换选中状态
ss.Checked = !ss.Checked;
lastClickTime = currentTime;
};
}
else
{
// 文件项的点击事件:切换选中状态
sender.Click += (sss, _) =>
{
var ss = (MyLocalCompItem)sss;
ss.Checked = !ss.Checked;
};
}
// 图标按钮
var btnOpen = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/folder-open", Tag = sender };
btnOpen.ToolTip = Lang.Text("Instance.Saves.OpenFileLocation");
ToolTipService.SetPlacement(btnOpen, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnOpen, 30d);
ToolTipService.SetHorizontalOffset(btnOpen, 2d);
btnOpen.Click += (ss, ee) => Open_Click((MyIconButton)ss, ee);
var btnCont = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/info", Tag = sender };
btnCont.ToolTip = Lang.Text("Instance.Saves.Detail");
ToolTipService.SetPlacement(btnCont, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnCont, 30d);
ToolTipService.SetHorizontalOffset(btnCont, 2d);
btnCont.Click += Info_Click;
sender.MouseRightButtonUp += Info_Click;
var btnDelete = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/trash-2", Tag = sender };
btnDelete.ToolTip = Lang.Text("Common.Action.Delete");
ToolTipService.SetPlacement(btnDelete, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnDelete, 30d);
ToolTipService.SetHorizontalOffset(btnDelete, 2d);
btnDelete.Click += (ss, ee) => Delete_Click((MyIconButton)ss, ee);
if (currentCompType != ModComp.CompType.Mod ||
sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable)
{
sender.Buttons = new[] { btnCont, btnOpen, btnDelete };
}
else
{
var btnED = new MyIconButton
{
LogoScale = 1d,
SvgIcon = sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine
? "lucide/circle-minus"
: "lucide/circle-check",
Tag = sender
};
btnED.ToolTip = sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? Lang.Text("Instance.Resource.Disable") : Lang.Text("Instance.Resource.Enable");
ToolTipService.SetPlacement(btnED, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnED, 30d);
ToolTipService.SetHorizontalOffset(btnED, 2d);
btnED.Click += (ss, ee) => ED_Click((MyIconButton)ss, ee);
sender.Buttons = new[] { btnCont, btnOpen, btnED, btnDelete };
}
}
/// <summary>
/// 刷新整个 UI。
/// </summary>
public void RefreshUI()
{
if (PanList is null)
return;
var showingMods = (IsSearching ? searchResult : modItems.Values.Select(i => i.Entry))
.Where(m => CanPassFilter(m)).ToList();
// 对显示的资源进行排序,确保文件夹置顶
if (showingMods.Any())
{
var sortMethod = GetSortMethod(currentSortMethod);
showingMods.Sort((a, b) => sortMethod(a, b));
}
// 重新列出列表
ModAnimation.AniControlEnabled += 1;
if (showingMods.Any())
{
PanList.Visibility = Visibility.Visible;
PanList.Children.Clear();
foreach (var TargetMod in showingMods)
{
if (!modItems.ContainsKey(TargetMod.RawPath))
continue;
var item = modItems[TargetMod.RawPath];
// 确保元素没有父容器,避免重复添加异常
if (item.Parent is not null) ((Panel)item.Parent).Children.Remove(item);
ModStyle.MinecraftFormatter.SetColorfulTextLab(item.LabTitle.Text, item.LabTitle,
ThemeService.IsDarkMode);
ModStyle.MinecraftFormatter.SetColorfulTextLab(item.LabInfo.Text, item.LabInfo,
ThemeService.IsDarkMode);
item.Checked = selectedMods.Contains(TargetMod.RawPath); // 更新选中状态
PanList.Children.Add(item);
}
}
else
{
PanList.Visibility = Visibility.Collapsed;
}
ModAnimation.AniControlEnabled -= 1;
selectedMods =
new HashSet<string>(selectedMods.Where(m => showingMods.Any(s => (s.RawPath ?? "") == (m ?? ""))));
RefreshBars();
}
/// <summary>
/// 刷新顶栏和底栏显示。
/// </summary>
public void RefreshBars()
{
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
// -----------------
// 顶部栏
// -----------------
// 计数
var anyCount = 0;
var enabledCount = 0;
var disabledCount = 0;
var updateCount = 0;
var unavalialeCount = 0;
var itemSource = (IsSearching ? searchResult : modItems.Values.Select(i => i.Entry)).ToArray();
await Task.Run(() =>
{
foreach (var item in itemSource)
{
anyCount += 1;
if (item.CanUpdate) updateCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) enabledCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) disabledCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) unavalialeCount += 1;
}
});
// 显示
BtnFilterAll.Text = IsSearching ? Lang.Text("Instance.Resource.Filter.SearchResult") : Lang.Text("Instance.Resource.Filter.AllWithCount", anyCount);
BtnFilterCanUpdate.Text = Lang.Text("Instance.Resource.Filter.UpdatableWithCount", updateCount);
BtnFilterCanUpdate.Visibility = Filter == FilterType.CanUpdate || updateCount > 0
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterEnabled.Text = Lang.Text("Instance.Resource.Filter.EnabledWithCount", enabledCount);
BtnFilterEnabled.Visibility = Filter == FilterType.Enabled || (enabledCount > 0 && enabledCount < anyCount)
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterDisabled.Text = Lang.Text("Instance.Resource.Filter.DisabledWithCount", disabledCount);
BtnFilterDisabled.Visibility = Filter == FilterType.Disabled || disabledCount > 0
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterError.Text = Lang.Text("Instance.Resource.Filter.ErrorWithCount", unavalialeCount);
BtnFilterError.Visibility = Filter == FilterType.Unavailable || unavalialeCount > 0
? Visibility.Visible
: Visibility.Collapsed;
// 查找重复项目
var duplicateItems = await Task.Run(() => itemSource.GroupBy(m =>
{
if (m.Comp is null) return ":Nothing:";
return m.Comp.Id;
}).Where(g => g.Count() > 1 && g.First().Comp is not null).SelectMany(g => g).ToList());
BtnFilterDuplicate.Text = Lang.Text("Instance.Resource.Filter.DuplicateWithCount", duplicateItems.Count);
BtnFilterDuplicate.Visibility = Filter == FilterType.Duplicate || duplicateItems.Any()
? Visibility.Visible
: Visibility.Collapsed;
// 返回按钮显示控制(在子文件夹中时显示)
if (!string.IsNullOrEmpty(CurrentFolderPath))
BtnManageBack.Visibility = Visibility.Visible;
else
BtnManageBack.Visibility = Visibility.Collapsed;
// -----------------
// 底部栏
// -----------------
// 计数
var newCount = selectedMods.Count;
var selected = newCount > 0;
if (selected)
LabSelect.Text = Lang.Text("Instance.Resource.SelectedCount", newCount); // 取消所有选择时不更新数字
// 按钮可用性
if (selected)
{
var hasUpdate = false;
var hasEnabled = false;
var hasDisabled = false;
var canFavoriteAndShare = true; // 是否可以收藏和分享
// 检查是否所有选中的资源都有有效的项目信息(即已完成联网更新)
await Task.Run(() =>
{
foreach (var ModEntity in ModLocalComp.compResourceListLoader.output)
if (selectedMods.Contains(ModEntity.RawPath))
{
if (ModEntity.CanUpdate) hasUpdate = true;
if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
hasEnabled = true;
else if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled)
hasDisabled = true;
if (ModEntity.Comp is null || string.IsNullOrEmpty(ModEntity.Comp.Id))
canFavoriteAndShare = false;
}
});
BtnSelectDisable.IsEnabled = hasEnabled;
BtnSelectEnable.IsEnabled = hasDisabled;
BtnSelectUpdate.IsEnabled = hasUpdate;
// 针对投影原理图隐藏分享 更新 收藏按钮
if (currentCompType == ModComp.CompType.Schematic)
{
BtnSelectUpdate.Visibility = Visibility.Collapsed;
BtnSelectFavorites.Visibility = Visibility.Collapsed;
BtnSelectShare.Visibility = Visibility.Collapsed;
}
else
{
BtnSelectUpdate.Visibility = Visibility.Visible;
BtnSelectFavorites.Visibility = Visibility.Visible;
BtnSelectShare.Visibility = Visibility.Visible;
// 根据是否已加载项目信息来启用/禁用收藏和分享按钮
BtnSelectFavorites.IsEnabled = canFavoriteAndShare;
BtnSelectShare.IsEnabled = canFavoriteAndShare;
}
}
// 更新显示状态
if (ModAnimation.AniControlEnabled == 0)
{
PanListBack.Margin = new Thickness(0d, 0d, 0d, selected ? 95 : 15);
if (selected)
{
// 仅在数量增加时播放出现/跳跃动画
if (bottomBarShownCount >= newCount)
{
bottomBarShownCount = newCount;
return;
}
bottomBarShownCount = newCount;
// 出现/跳跃动画
CardSelect.Visibility = Visibility.Visible;
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(CardSelect, 1d - CardSelect.Opacity, 60),
ModAnimation.AaTranslateY(CardSelect, -27 - TransSelect.Y, 120,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaTranslateY(CardSelect, 3d, 150, 120,
new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaTranslateY(CardSelect, -1, 90, 270,
new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak))
}, "Mod Sidebar");
}
else
{
// 不重复播放隐藏动画
if (bottomBarShownCount == 0)
return;
bottomBarShownCount = 0;
// 隐藏动画
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(CardSelect, -CardSelect.Opacity, 90),
ModAnimation.AaTranslateY(CardSelect, -10 - TransSelect.Y, 90,
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaCode(() => CardSelect.Visibility = Visibility.Collapsed, after: true)
}, "Mod Sidebar");
}
}
else
{
ModAnimation.AniStop("Mod Sidebar");
bottomBarShownCount = newCount;
if (selected)
{
CardSelect.Visibility = Visibility.Visible;
CardSelect.Opacity = 1d;
TransSelect.Y = -25;
}
else
{
CardSelect.Visibility = Visibility.Collapsed;
CardSelect.Opacity = 0d;
TransSelect.Y = -10;
}
}
}));
}
private int bottomBarShownCount;
#endregion
#region
/// <summary>
/// 打开 Mods 文件夹。
/// </summary>
private void BtnManageBack_Click(object sender, EventArgs e)
{
GoBackToParentFolder();
}
private void BtnHintBack_Click(object sender, EventArgs e)
{
GoBackToParentFolder();
}
private void BtnManageOpen_Click(object sender, EventArgs e)
{
try
{
string compFilePath;
// 如果当前在子文件夹中,则打开当前子文件夹;否则打开根目录
if (string.IsNullOrEmpty(CurrentFolderPath))
// 打开根目录
compFilePath = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
else
// 打开当前子文件夹
compFilePath = CurrentFolderPath.EndsWith(@"\") ? CurrentFolderPath : CurrentFolderPath + @"\";
Directory.CreateDirectory(compFilePath);
ModBase.OpenExplorer(compFilePath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"打开 Mods 文件夹失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
/// <summary>
/// 全选。
/// </summary>
private void BtnManageSelectAll_Click(object sender, MouseButtonEventArgs e)
{
ChangeAllSelected(selectedMods.Count < PanList.Children.Count);
}
/// <summary>
/// 安装 Mod。
/// </summary>
private void BtnManageInstall_Click(object sender, MouseButtonEventArgs e)
{
string[] fileList = null;
switch (currentCompType)
{
case ModComp.CompType.Mod:
{
fileList = SystemDialogs.SelectFiles(
Lang.Text("Instance.Resource.Install.FileDialog.Mod.Filter"),
Lang.Text("Instance.Resource.Install.FileDialog.Mod.Title"));
break;
}
case ModComp.CompType.ResourcePack:
{
fileList = SystemDialogs.SelectFiles(
Lang.Text("Instance.Resource.Install.FileDialog.ResourcePack.Filter"),
Lang.Text("Instance.Resource.Install.FileDialog.ResourcePack.Title"));
break;
}
case ModComp.CompType.Shader:
{
fileList = SystemDialogs.SelectFiles(
Lang.Text("Instance.Resource.Install.FileDialog.Shader.Filter"),
Lang.Text("Instance.Resource.Install.FileDialog.Shader.Title"));
break;
}
case ModComp.CompType.Schematic:
{
fileList = SystemDialogs.SelectFiles(
Lang.Text("Instance.Resource.Install.FileDialog.Schematic.Filter"),
Lang.Text("Instance.Resource.Install.FileDialog.Schematic.Title"));
break;
}
}
if (fileList is null || !fileList.Any())
return;
InstallCompFiles(fileList, currentCompType, CurrentFolderPath);
}
/// <summary>
/// 尝试安装 Mod。
/// 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。
/// </summary>
public static bool InstallMods(IEnumerable<string> filePathList)
{
if (!filePathList.Any()) return false;
// 1. Check file extension
var firstFile = filePathList.First();
var extension = firstFile.Split('.').LastOrDefault()?.ToLower();
string[] allowedExtensions = { "jar", "litemod", "disabled", "old" };
if (!allowedExtensions.Contains(extension)) return false;
LogWrapper.Info("[System] 文件格式为 jar/litemod,尝试安装为 Mod");
// 2. Check recycle bin
if (firstFile.Contains(@":\$RECYCLE.BIN\"))
{
HintWrapper.Show(Lang.Text("Instance.Resource.Install.RestoreFromRecycleBin"), HintTheme.Error);
return true;
}
// 3. Determine target instance
var targetInstance = ModInstanceList.McMcInstanceSelected;
if (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup) targetInstance = PageInstanceLeft.McInstance;
// 4. Validate instance status
if (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSelect || targetInstance is null ||
!targetInstance.Modable)
{
HintWrapper.Show(Lang.Text("Instance.Resource.Install.SelectModableInstance"));
return true;
}
// 5. Check if user confirmation is required
var isModPage = ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup &&
ModMain.frmMain.PageCurrentSub == FormMain.PageSubType.VersionMod;
if (!isModPage)
{
if (ModMain.MyMsgBox(Lang.Text("Instance.Resource.Install.ModConfirm.Message", targetInstance.Name), Lang.Text("Instance.Resource.Install.ModConfirm.Title"), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) !=
1) return true;
}
// 6. Execution: Install Mods
ExecuteModInstallation(targetInstance, filePathList, isModPage);
return true;
}
private static void ExecuteModInstallation(McInstance targetMcInstance, IEnumerable<string> filePathList,
bool refreshList)
{
// Path resolution logic
var modPathSuffix = targetMcInstance.Info.HasLabyMod
? $@"labymod-neo\fabric\{targetMcInstance.Info.VanillaName}\"
: "";
var modFolder = $@"{targetMcInstance.PathIndie}{modPathSuffix}mods\";
try
{
foreach (var modFile in filePathList)
{
var fileName = ModBase.GetFileNameFromPath(modFile)
.Replace(".disabled", "")
.Replace(".old", "");
if (!fileName.Contains(".")) fileName += ".jar"; // Ensure extension (#4227)
ModBase.CopyFile(modFile, Path.Combine(modFolder, fileName));
}
// Success hint
if (filePathList.Count() == 1)
{
var installedName = ModBase.GetFileNameFromPath(filePathList.First()).Replace(".disabled", "")
.Replace(".old", "");
HintWrapper.Show(Lang.Text("Instance.Resource.Install.SuccessSingle", installedName), HintTheme.Success);
}
else
{
HintWrapper.Show(Lang.Text("Instance.Resource.Install.SuccessMultiple", filePathList.Count(), Lang.Text("Download.Comp.Type.Mod")), HintTheme.Success);
}
// 7. Refresh list if necessary
if (refreshList)
ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader,
modFolder,
ModLoader.LoaderFolderRunType.ForceRun,
loaderInput: ModMain.frmInstanceMod.GetRequireLoaderData()
);
}
catch (Exception ex)
{
LogWrapper.Error(ex, "拷贝文件失败");
}
}
/// <summary>
/// 安装组件文件(Mod、资源包、光影包、投影文件等)。
/// </summary>
public static void InstallCompFiles(IEnumerable<string> filePathList, ModComp.CompType compType,
string targetFolderPath = "")
{
if (!filePathList.Any())
return;
var extension = filePathList.First().AfterLast(".").ToLower();
string[] validExtensions = null;
var compTypeName = "";
var compFolder = "";
// 检查回收站:回收站中的文件有错误的文件名
if (filePathList.First().Contains(@":\$RECYCLE.BIN\"))
{
HintService.Hint(Lang.Text("Instance.Resource.Install.RestoreFromRecycleBin"), HintType.Error);
return;
}
// 获取并检查目标实例
var targetInstance = ModInstanceList.McMcInstanceSelected;
if (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup)
targetInstance = PageInstanceLeft.McInstance;
// 根据组件类型设置相关参数
switch (compType)
{
case ModComp.CompType.Mod:
{
validExtensions = new[] { "jar", "litemod", "disabled", "old" };
compTypeName = "Mod";
if (string.IsNullOrEmpty(targetFolderPath))
compFolder = targetInstance.PathIndie +
(targetInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", targetInstance.Info.VanillaName)
: "") + @"mods\";
else
compFolder = targetFolderPath;
break;
}
case ModComp.CompType.ResourcePack:
{
validExtensions = new[] { "zip" };
compTypeName = Lang.Text("Download.Comp.Type.ResourcePack");
if (string.IsNullOrEmpty(targetFolderPath))
compFolder = targetInstance.PathIndie + @"resourcepacks\";
else
compFolder = targetFolderPath;
break;
}
case ModComp.CompType.Shader:
{
validExtensions = new[] { "zip" };
compTypeName = Lang.Text("Download.Comp.Type.Shader");
if (string.IsNullOrEmpty(targetFolderPath))
compFolder = targetInstance.PathIndie + @"shaderpacks\";
else
compFolder = targetFolderPath;
break;
}
case ModComp.CompType.Schematic:
{
validExtensions = new[] { "litematic", "nbt", "schematic", "schem" };
compTypeName = Lang.Text("Download.Comp.Type.Schematic");
if (string.IsNullOrEmpty(targetFolderPath))
compFolder = targetInstance.PathIndie + @"schematics\";
else
compFolder = targetFolderPath;
break;
}
}
// 检查文件扩展名
if (!validExtensions.Contains(extension))
{
HintService.Hint(Lang.Text("Instance.Resource.Install.UnsupportedFormat", extension, compTypeName, string.Join(", ", validExtensions)),
HintType.Error);
return;
}
ModBase.Log($"[System] 文件为 {extension} 格式,尝试作为{compTypeName}安装");
// 检查实例兼容性
if (compType == ModComp.CompType.Mod && (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSelect ||
targetInstance is null || !targetInstance.Modable))
{
HintService.Hint(Lang.Text("Instance.Resource.Install.SelectModableInstance"));
return;
}
// 确认安装
var currentPage = FormMain.PageSubType.VersionMod;
switch (compType)
{
case ModComp.CompType.Mod:
{
currentPage = FormMain.PageSubType.VersionMod;
break;
}
case ModComp.CompType.ResourcePack:
{
currentPage = FormMain.PageSubType.VersionResourcePack;
break;
}
case ModComp.CompType.Shader:
{
currentPage = FormMain.PageSubType.VersionShader;
break;
}
case ModComp.CompType.Schematic:
{
currentPage = FormMain.PageSubType.VersionSchematic;
break;
}
}
if (!(ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup &&
ModMain.frmMain.PageCurrentSub == currentPage))
if (ModMain.MyMsgBox(
Lang.Text("Instance.Resource.Install.GenericConfirm.Message", compTypeName, targetInstance.Name),
Lang.Text("Instance.Resource.Install.GenericConfirm.Title", compTypeName), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) != 1)
return;
// 执行安装
try
{
Directory.CreateDirectory(compFolder);
foreach (var FilePath in filePathList)
{
var newFileName = ModBase.GetFileNameFromPath(FilePath);
if (compType == ModComp.CompType.Mod)
{
newFileName = newFileName.Replace(".disabled", "").Replace(".old", "");
if (!newFileName.Contains("."))
newFileName += ".jar";
}
var destFile = compFolder + newFileName;
if (File.Exists(destFile))
if (ModMain.MyMsgBox(Lang.Text("Instance.Resource.Install.OverwriteConfirm.Message", newFileName), Lang.Text("Instance.Resource.Install.OverwriteConfirm.Title"), Lang.Text("Common.Action.Overwrite"), Lang.Text("Common.Action.Cancel")) != 1)
continue;
ModBase.CopyFile(FilePath, destFile);
}
if (filePathList.Count() == 1)
HintService.Hint(Lang.Text("Instance.Resource.Install.SuccessSingle", ModBase.GetFileNameFromPath(filePathList.First())), HintType.Success);
else
HintService.Hint(Lang.Text("Instance.Resource.Install.SuccessMultiple", filePathList.Count(), compTypeName), HintType.Success);
// 刷新列表
if (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup &&
ModMain.frmMain.PageCurrentSub == currentPage)
switch (compType)
{
case ModComp.CompType.Mod:
{
if (ModMain.frmInstanceMod is not null)
ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader, compFolder,
ModLoader.LoaderFolderRunType.ForceRun,
loaderInput: ModMain.frmInstanceMod?.GetRequireLoaderData());
break;
}
case ModComp.CompType.ResourcePack:
case ModComp.CompType.Shader:
case ModComp.CompType.Schematic:
{
var currentForm = GetCurrentCompResourceForm();
if (currentForm is not null) ModBase.RunInUi(() => currentForm.ReloadCompFileList(true));
break;
}
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"复制{compTypeName}文件失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
/// <summary>
/// 获取当前的组件资源管理窗体。
/// </summary>
private static PageInstanceCompResource GetCurrentCompResourceForm()
{
switch (ModMain.frmMain.PageCurrentSub)
{
case FormMain.PageSubType.VersionMod:
{
return ModMain.frmInstanceMod;
}
case FormMain.PageSubType.VersionResourcePack:
{
return ModMain.frmInstanceResourcePack;
}
case FormMain.PageSubType.VersionShader:
{
return ModMain.frmInstanceShader;
}
case FormMain.PageSubType.VersionSchematic:
{
return ModMain.frmInstanceSchematic;
}
default:
{
return null;
}
}
}
private void BtnManageInfoExport_Click(object sender, MouseButtonEventArgs e)
{
var choice =
ModMain.MyMsgBox(
Lang.Text("Instance.Resource.Export.Mode.Message"), Lang.Text("Instance.Resource.Export.Mode.Title"), Lang.Text("Instance.Resource.Export.Mode.Txt"), Lang.Text("Instance.Resource.Export.Mode.Csv"), Lang.Text("Common.Action.Cancel"));
void ExportText(string content, string fileName)
{
try
{
var savePath =
SystemDialogs.SelectSaveFile(Lang.Text("Instance.Resource.Export.SelectSaveLocation"), fileName, Lang.Text("Instance.Resource.Export.FilesFilter"));
if (string.IsNullOrWhiteSpace(savePath)) return;
File.WriteAllText(savePath, content, Encoding.UTF8);
ModBase.OpenExplorer(savePath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"导出资源信息失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
;
switch (choice)
{
case 1: // TXT
{
var exportContent = new List<string>();
foreach (var ModEntity in ModLocalComp.compResourceListLoader.output)
{
exportContent.Add(ModEntity.FileName);
_AppendEmbeddedForExport(exportContent, ModEntity.EmbeddedMods, 1);
}
ExportText(exportContent.Join("\r\n"), PageInstanceLeft.McInstance.Name + "已安装的资源信息.txt");
break;
}
case 2: // CSV
{
var exportContent = new List<string>();
exportContent.Add("文件名,资源名称,资源版本,此版本更新时间,Mod ID,对应平台工程 ID,文件大小(字节),文件路径,内嵌模组");
foreach (var ModEntity in ModLocalComp.compResourceListLoader.output)
exportContent.Add(
$"{ModEntity.FileName},{ModEntity.Comp?.TranslatedName},{ModEntity.Version},{ModEntity.compFile?.ReleaseDate},{ModEntity.ModId},{ModEntity.Comp?.Id},{GetModFileInfo(ModEntity.path).Length},{ModEntity.path},{string.Join(";", _FlattenEmbeddedNames(ModEntity.EmbeddedMods))}");
ExportText(exportContent.Join("\r\n"), PageInstanceLeft.McInstance.Name + "已安装的资源信息.csv");
break;
}
}
}
private static void _AppendEmbeddedForExport(List<string> lines, List<ModLocalComp.LocalCompFile> mods, int depth)
{
var indent = new string('\t', depth);
foreach (var mod in mods)
{
var line = indent + "└ " + (mod.Name ?? mod.ModId ?? mod.FileName);
if (!string.IsNullOrWhiteSpace(mod.Version))
line += $" ({mod.Version})";
lines.Add(line);
if (mod.EmbeddedMods is { Count: > 0 })
_AppendEmbeddedForExport(lines, mod.EmbeddedMods, depth + 1);
}
}
private static IEnumerable<string> _FlattenEmbeddedNames(List<ModLocalComp.LocalCompFile> mods)
{
foreach (var mod in mods)
{
yield return mod.Name ?? mod.ModId ?? mod.FileName;
if (mod.EmbeddedMods is { Count: > 0 })
foreach (var child in _FlattenEmbeddedNames(mod.EmbeddedMods))
yield return child;
}
}
/// <summary>
/// 下载 Mod。
/// </summary>
private void BtnManageDownload_Click(object sender, MouseButtonEventArgs e)
{
switch (currentCompType)
{
case ModComp.CompType.Mod:
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod);
break;
}
case ModComp.CompType.ResourcePack:
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadResourcePack);
break;
}
case ModComp.CompType.Shader:
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadShader);
break;
}
}
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
}
/// <summary>
/// 下载投影Mod按钮点击事件。
/// </summary>
private void BtnSchematicDownloadMod_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod);
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
}
/// <summary>
/// 实例选择按钮点击事件。
/// </summary>
private void BtnSchematicVersionSelect_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Launch);
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect);
}
#endregion
#region
/// <summary>
/// 选择的 Mod 的路径(不含 .disabled 和 .old)。
/// </summary>
public HashSet<string> selectedMods = new();
// 单项切换选择状态
public void CheckChanged(MyLocalCompItem sender, ModBase.RouteEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
// 更新选择了的内容
var selectedKey = sender.Entry.RawPath;
if (sender.Checked)
selectedMods.Add(selectedKey);
else
selectedMods.Remove(selectedKey);
RefreshBars();
}
// 切换所有项的选择状态
private void ChangeAllSelected(bool value)
{
ModAnimation.AniControlEnabled += 1;
selectedMods.Clear();
foreach (var Item in modItems.Values)
{
// #4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断
var shouldSelected = value && PanList.Children.Contains(Item);
Item.Checked = shouldSelected;
if (shouldSelected)
selectedMods.Add(Item.Entry.RawPath);
}
ModAnimation.AniControlEnabled -= 1;
RefreshBars();
}
private void UnselectedAllWithAnimation()
{
var cacheAniControlEnabled = ModAnimation.AniControlEnabled;
ModAnimation.AniControlEnabled = 0;
ChangeAllSelected(false);
ModAnimation.AniControlEnabled += cacheAniControlEnabled;
}
private void FrmMain_KeyDown(object sender, KeyEventArgs e) // 若监听自己的事件则在进入页面后需点击右侧控件才可监听到 (#4311)
{
if (!ReferenceEquals(ModMain.frmMain.pageRight, this))
return;
if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A)
ChangeAllSelected(true);
}
private void SearchBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
// Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获
if (SearchBox.Text.Any())
return;
if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A)
PanBack.Focus();
}
#endregion
#region
public FilterType Filter
{
get => field;
set
{
if (field == value)
return;
field = value;
switch (value)
{
case FilterType.All:
{
BtnFilterAll.Checked = true;
break;
}
case FilterType.Enabled:
{
BtnFilterEnabled.Checked = true;
break;
}
case FilterType.Disabled:
{
BtnFilterDisabled.Checked = true;
break;
}
case FilterType.CanUpdate:
{
BtnFilterCanUpdate.Checked = true;
break;
}
case FilterType.Duplicate:
{
BtnFilterDuplicate.Checked = true;
break;
}
default:
{
BtnFilterError.Checked = true;
break;
}
}
RefreshUI();
}
} = FilterType.All;
public enum FilterType
{
All = 0,
Enabled = 1,
Disabled = 2,
CanUpdate = 3,
Unavailable = 4,
Duplicate = 5
}
/// <summary>
/// 检查该 Mod 项是否符合当前筛选的类别。
/// </summary>
private bool CanPassFilter(ModLocalComp.LocalCompFile checkingMod)
{
switch (Filter)
{
case FilterType.All:
{
return true;
}
case FilterType.Enabled:
{
return checkingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine;
}
case FilterType.Disabled:
{
return checkingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled;
}
case FilterType.CanUpdate:
{
return checkingMod.CanUpdate;
}
case FilterType.Unavailable:
{
return checkingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable;
}
case FilterType.Duplicate:
{
var itemSource = IsSearching
? searchResult
: ModLocalComp.compResourceListLoader.output ?? new List<ModLocalComp.LocalCompFile>();
return itemSource is not null && itemSource.Where(m =>
checkingMod.Comp is not null && m.Comp is not null &&
(checkingMod.Comp.Id ?? "") == (m.Comp.Id ?? "")).Skip(1).Any();
}
default:
{
return false;
}
}
}
// 点击筛选项触发的改变
private void ChangeFilter(MyRadioButton sender, bool raiseByMouse)
{
Filter = (FilterType)Convert.ToInt32(sender.Tag);
RefreshUI();
DoSort();
}
#endregion
#region
private SortMethod currentSortMethod = SortMethod.CompName;
private void SetSortMethod(SortMethod target)
{
currentSortMethod = target;
BtnSort.Text = Lang.Text("Instance.Resource.Sort.Text", GetSortName(target));
// RefreshUI()
DoSort();
}
private enum SortMethod
{
FileName,
CompName,
TagNums,
CreateTime,
ModFileSize
}
private string GetSortName(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return Lang.Text("Instance.Resource.Sort.FileName");
}
case SortMethod.CompName:
{
return Lang.Text("Instance.Resource.Sort.ResourceName");
}
case SortMethod.TagNums:
{
return Lang.Text("Instance.Resource.Sort.TagCount");
}
case SortMethod.CreateTime:
{
return Lang.Text("Instance.Resource.Sort.AddTime");
}
case SortMethod.ModFileSize:
{
return Lang.Text("Instance.Resource.Sort.FileSize");
}
default:
{
return Lang.Text("Instance.Resource.Sort.ResourceName");
}
}
return "";
}
private void BtnSortClick(object sender, ModBase.RouteEventArgs e)
{
var body = new ContextMenu();
foreach (SortMethod i in Enum.GetValues(typeof(SortMethod)))
{
var item = new MyMenuItem();
item.Header = GetSortName(i);
item.Click += (_, _) => SetSortMethod(i);
body.Items.Add(item);
}
body.PlacementTarget = (UIElement)sender;
body.Placement = PlacementMode.Bottom;
body.IsOpen = true;
}
private readonly object sortLock = new();
private void DoSort()
{
lock (sortLock)
{
try
{
if (PanList is null || PanList.Children.Count < 2)
return;
// 将子元素转换为可排序的列表
var items = PanList.Children.OfType<MyLocalCompItem>().ToList();
var method = GetSortMethod(currentSortMethod);
// 分离有效和无效项(保持原始相对顺序)
var invalid = items.Where(i =>
i.Entry is null || (currentSortMethod == SortMethod.TagNums && i.Entry.Comp is null &&
!i.Entry.IsFolder)).ToList();
var valid = items.Except(invalid).ToList();
// 仅对有效项进行排序
valid.Sort((x, y) => method(x.Entry, y.Entry));
// 合并保持无效项的原始顺序
items = valid.Concat(invalid).ToList();
// 批量更新UI元素
PanList.Children.Clear();
items.ForEach(i => PanList.Children.Add(i));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"执行排序时出错",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
}
private Func<ModLocalComp.LocalCompFile, ModLocalComp.LocalCompFile, int> GetSortMethod(SortMethod method)
{
// 通用的文件夹置顶比较函数
int folderFirstCompare(ModLocalComp.LocalCompFile a, ModLocalComp.LocalCompFile b)
{
if (a.IsFolder && !b.IsFolder)
return -1;
if (!a.IsFolder && b.IsFolder)
return 1;
return 0; // 相同类型,需要进一步比较
}
;
switch (method)
{
case SortMethod.FileName:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹或都是文件,则按文件名排序
return string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase);
};
}
case SortMethod.CompName:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹或都是文件,则按资源名称排序
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
};
}
case SortMethod.TagNums:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹,则按名称排序
if (a.IsFolder && b.IsFolder)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
// 如果都是文件,则按标签数量排序(标签多的在前)
if (!a.IsFolder && !b.IsFolder)
{
// 安全检查,确保Comp不为空
var aTagCount = a.Comp?.Tags?.Count ?? 0;
var bTagCount = b.Comp?.Tags?.Count ?? 0;
return bTagCount.CompareTo(aTagCount);
}
// 理论上不会到达这里,但为了安全起见
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
};
}
case SortMethod.CreateTime:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹或都是文件,则按创建时间排序(新的在前)
var aPath = a.IsFolder ? a.ActualPath : a.path;
var bPath = b.IsFolder ? b.ActualPath : b.path;
var aDate = GetModFileInfo(aPath).CreationTime;
var bDate = GetModFileInfo(bPath).CreationTime;
if (aDate == DateTime.MinValue && bDate == DateTime.MinValue)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
if (aDate == DateTime.MinValue) return 1; // 出错的文件排在后面
if (bDate == DateTime.MinValue) return -1;
return bDate.CompareTo(aDate);
};
}
case SortMethod.ModFileSize:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹,则按名称排序
if (a.IsFolder && b.IsFolder)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
// 如果都是文件,则按文件大小排序(大的在前)
if (!a.IsFolder && !b.IsFolder)
{
var aSize = GetModFileInfo(a.ActualPath).Length;
var bSize = GetModFileInfo(b.ActualPath).Length;
if (aSize == 0L && bSize == 0L)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
if (aSize == 0L) return 1;
if (bSize == 0L) return -1;
return bSize.CompareTo(aSize);
}
// 理论上不会到达这里,但为了安全起见
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
};
}
default:
{
return (a, b) =>
{
// 文件夹始终排在最前面
var folderResult = folderFirstCompare(a, b);
if (folderResult != 0)
return folderResult;
// 如果都是文件夹或都是文件,则按名称排序
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
};
}
}
}
#endregion
#region
// 启用 / 禁用
private void BtnSelectED_Click(object sender, ModBase.RouteEventArgs e)
{
EDMods(ModLocalComp.compResourceListLoader.output.Where(m => selectedMods.Contains(m.RawPath)).ToList(),
!sender.Equals(BtnSelectDisable));
ChangeAllSelected(false);
}
private void EDMods(IEnumerable<ModLocalComp.LocalCompFile> modList, bool isEnable)
{
var isSuccessful = true;
foreach (var ModE in modList)
{
var modEntity = ModE; // 仅用于去除迭代变量无法修改的限制
string newPath = null;
if (modEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine && !isEnable)
// 禁用
newPath = modEntity.path + (File.Exists(modEntity.path + ".old") ? ".old" : ".disabled");
else if (modEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled && isEnable)
// 启用
newPath = modEntity.RawPath;
else
continue;
// 重命名
try
{
if (File.Exists(newPath))
{
if (File.Exists(modEntity.path))
{
// 同时存在两个名称的 Mod
if ((ModBase.GetFileMD5(modEntity.path) ?? "") != (ModBase.GetFileMD5(newPath) ?? ""))
{
ModMain.MyMsgBox(
Lang.Text("Instance.Resource.Ed.FileConflict.Message", newPath, modEntity.path),
Lang.Text("Instance.Resource.Ed.FileConflict"));
continue;
}
}
else
{
// 已经重命名过了
ModBase.Log("[Mod] Mod 的状态已被切换", ModBase.LogLevel.Debug);
continue;
}
}
File.Delete(newPath);
FileSystem.Rename(modEntity.path, newPath);
}
catch (FileNotFoundException ex)
{
ModBase.Log(
ex,
$"未找到需要重命名的 Mod{modEntity.path ?? "null"}",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
ReloadCompFileList(true);
return;
}
catch (Exception ex)
{
ModBase.Log(ex, $"重命名 Mod 失败({modEntity.path ?? "null"}");
isSuccessful = false;
}
// 更改 Loader 中的列表
var newModEntity = new ModLocalComp.LocalCompFile(newPath);
newModEntity.FromJson(modEntity.ToJson());
if (ModLocalComp.compResourceListLoader.output.Contains(modEntity))
{
var indexOfLoader = ModLocalComp.compResourceListLoader.output.IndexOf(modEntity);
ModLocalComp.compResourceListLoader.output.RemoveAt(indexOfLoader);
ModLocalComp.compResourceListLoader.output.Insert(indexOfLoader, newModEntity);
}
if (searchResult is not null && searchResult.Contains(modEntity)) // #4862
{
var indexOfResult = searchResult.IndexOf(modEntity);
searchResult.Remove(modEntity);
searchResult.Insert(indexOfResult, newModEntity);
}
// 更改 UI 中的列表
try
{
var newItem = BuildLocalCompItem(newModEntity);
modItems[modEntity.RawPath] = newItem;
var indexOfUi = PanList.Children.IndexOf(PanList.Children.OfType<MyLocalCompItem>()
.FirstOrDefault(i => ReferenceEquals(i.Entry, modEntity)));
if (indexOfUi == -1)
continue; // 因为未知原因 Mod 的状态已经切换完了
PanList.Children.RemoveAt(indexOfUi);
PanList.Children.Insert(indexOfUi, newItem);
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"更新 UI 列表项失败:{modEntity.FileName}",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
Dispatcher.Invoke(() => PanList.UpdateLayout(), DispatcherPriority.Background);
if (isSuccessful)
{
RefreshBars();
}
else
{
HintService.Hint(Lang.Text("Instance.Resource.Ed.ToggleFailed"), HintType.Error);
ReloadCompFileList(true);
}
LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly);
}
// 更新
private void BtnSelectUpdate_Click(object sender, ModBase.RouteEventArgs e)
{
var updateList = ModLocalComp.compResourceListLoader.output
.Where(m => selectedMods.Contains(m.RawPath) && m.CanUpdate).ToList();
if (!updateList.Any())
return;
UpdateResource(updateList);
ChangeAllSelected(false);
}
/// <summary>
/// 记录正在进行 Mod 更新的 mods 文件夹路径。
/// </summary>
public static List<string> updatingVersions = new();
public void UpdateResource(IEnumerable<ModLocalComp.LocalCompFile> modList)
{
// 更新前警告
if (currentCompType == ModComp.CompType.Mod && (!States.Hint.UpdateMod || modList.Count() >= 15))
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Resource.Update.Warning.Message"),
Lang.Text("Instance.Resource.Update.Warning.Title"), Lang.Text("Instance.Resource.Update.Warning.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 1)
States.Hint.UpdateMod = true;
else
return;
}
try
{
// 构造下载信息
modList = modList.ToList(); // 防止刷新影响迭代器
var fileList = new List<DownloadFile>();
var fileCopyList = new Dictionary<string, string>();
foreach (var Entry in modList)
{
var file = Entry.UpdateFile;
if (!file.Available)
continue;
// 确认更新后的文件名
var currentReplaceName = Entry.compFile.FileName.Replace(".jar", "").Replace(".old", "")
.Replace(".disabled", "");
var newestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "")
.Replace(".disabled", "");
var currentSegs = currentReplaceName.Split('-').ToList();
var newestSegs = newestReplaceName.Split('-').ToList();
var shortened = false;
while (true) // 移除前导相同部分(不能移除所有相同项,这会导致例如 1.2-forge-2 和 1.3-forge-3 中间的 forge 被去掉,导致尝试替换 1.2-2)
{
if (!currentSegs.Any() || !newestSegs.Any())
break;
if ((currentSegs.First() ?? "") != (newestSegs.First() ?? ""))
break;
currentSegs.RemoveAt(0);
newestSegs.RemoveAt(0);
shortened = true;
}
while (true) // 移除后导相同部分
{
if (!currentSegs.Any() || !newestSegs.Any())
break;
if ((currentSegs.Last() ?? "") != (newestSegs.Last() ?? ""))
break;
currentSegs.RemoveAt(currentSegs.Count - 1);
newestSegs.RemoveAt(newestSegs.Count - 1);
shortened = true;
}
if (shortened && currentSegs.Any() && newestSegs.Any())
{
currentReplaceName = currentSegs.Join("-");
newestReplaceName = newestSegs.Join("-");
}
// 添加到下载列表
var tempAddress = ModBase.pathTemp + @"DownloadedComp\" +
Entry.FileName.Replace(currentReplaceName, newestReplaceName);
var realAddress = ModBase.GetPathFromFullPath(Entry.path) +
Entry.FileName.Replace(currentReplaceName, newestReplaceName);
fileList.Add(file.ToNetFile(tempAddress, ModComp.DownloadReason.Update));
fileCopyList[tempAddress] = realAddress;
}
// 构造加载器
var installLoaders = new List<ModLoader.LoaderBase>();
var finishedFileNames = new List<string>();
installLoaders.Add(new LoaderDownload(Lang.Text("Instance.Resource.Update.Task.DownloadFiles"), fileList)
{ ProgressWeight = modList.Count() * 1.5d }); // 每个 Mod 需要 1.5s
installLoaders.Add(new ModLoader.LoaderTask<int, int>(
Lang.Text("Instance.Resource.Update.Task.ReplaceFiles"), _ =>
{
try
{
foreach (var Entry in modList)
if (File.Exists(Entry.path))
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.path, UIOption.AllDialogs,
RecycleOption.SendToRecycleBin);
else
ModBase.Log($"[CompUpdate] 未找到更新前的资源文件,跳过对它的删除:{Entry.path}", ModBase.LogLevel.Debug);
foreach (var Entry in fileCopyList)
{
if (File.Exists(Entry.Value))
{
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Value, UIOption.AllDialogs,
RecycleOption.SendToRecycleBin);
ModBase.Log($"[Mod] 更新后的资源文件已存在,将会把它放入回收站:{Entry.Value}", ModBase.LogLevel.Debug);
}
if (Directory.Exists(ModBase.GetPathFromFullPath(Entry.Value)))
{
File.Move(Entry.Key, Entry.Value);
finishedFileNames.Add(ModBase.GetFileNameFromPath(Entry.Value));
}
else
{
ModBase.Log($"[Mod] 更新后的目标文件夹已被删除:{Entry.Value}", ModBase.LogLevel.Debug);
}
}
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "替换旧版资源文件时被主动取消");
}
}));
// 结束处理
var loader =
new ModLoader.LoaderCombo<IEnumerable<ModLocalComp.LocalCompFile>>(
Lang.Text("Instance.Resource.Update.Task.Title", PageInstanceLeft.McInstance.Name), installLoaders);
var pathMods = PageInstanceLeft.McInstance.PathIndie +
(PageInstanceLeft.McInstance.Info.HasLabyMod
? Path.Combine("labymod-neo", "fabric", PageInstanceLeft.McInstance.Info.VanillaName)
: "") + ModLocalComp.GetPathNameByCompType(currentCompType) + @"\";
loader.OnStateChanged = _ =>
{
// 结果提示
switch (loader.State)
{
case ModBase.LoadState.Finished:
{
switch (finishedFileNames.Count)
{
case 0: // 一般是由于 Mod 文件被占用,然后玩家主动取消
{
ModBase.Log("[CompUpdate] 没有资源被成功更新");
break;
}
case 1:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.SuccessSingle", finishedFileNames.Single()), HintType.Success);
break;
}
default:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.SuccessMultiple", finishedFileNames.Count), HintType.Success);
break;
}
}
break;
}
case ModBase.LoadState.Failed:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.Failed", loader.Error.Message), HintType.Error);
break;
}
case ModBase.LoadState.Aborted:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.Aborted"));
break;
}
default:
{
return;
}
}
ModBase.Log($"[CompUpdate] 已从正在进行资源更新的文件夹列表移除:{pathMods}");
updatingVersions.Remove(pathMods);
// 清理缓存
ModBase.RunInNewThread(() =>
{
try
{
foreach (var TempFile in fileCopyList.Keys)
if (File.Exists(TempFile))
File.Delete(TempFile);
}
catch (Exception ex)
{
ModBase.Log(ex, "清理资源更新缓存失败");
}
}, "Clean Comp Update Cache", ThreadPriority.BelowNormal);
};
// 启动加载器
ModBase.Log($"[CompUpdate] 开始更新 {modList.Count()} 个资源:{pathMods}");
updatingVersions.Add(pathMods);
loader.Start();
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
ReloadCompFileList(true);
}
catch (Exception ex)
{
ModBase.Log(ex, "初始化资源更新失败");
}
}
// 删除
private void BtnSelectDelete_Click(object sender, ModBase.RouteEventArgs e)
{
DeleteMods(ModLocalComp.compResourceListLoader.output.Where(m => selectedMods.Contains(m.RawPath)));
ChangeAllSelected(false);
}
private void DeleteMods(IEnumerable<ModLocalComp.LocalCompFile> modList)
{
try
{
var isSuccessful = true;
var isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
// 确认需要删除的文件
// 文件夹只需要删除自身
modList = modList.SelectMany(target =>
{
if (target.IsFolder) return new[] { target.path };
if (target.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
return new[]
{ target.path, target.path + (File.Exists(target.path + ".old") ? ".old" : ".disabled") };
return new[] { target.path, target.RawPath };
}).Distinct()
.Where(m => m.EndsWithF(@"\__FOLDER__", true)
? Directory.Exists(m.Replace(@"\__FOLDER__", ""))
: File.Exists(m)).Select(m => new ModLocalComp.LocalCompFile(m)).ToList();
// 实际删除文件
foreach (var ModEntity in modList)
{
// 删除
try
{
if (ModEntity.IsFolder)
{
// 删除文件夹
if (isShiftPressed)
Directory.Delete(ModEntity.ActualPath, true);
else
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(ModEntity.ActualPath,
UIOption.AllDialogs, RecycleOption.SendToRecycleBin);
}
// 删除文件
else if (isShiftPressed)
{
File.Delete(ModEntity.path);
}
else
{
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(ModEntity.path, UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);
}
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "删除资源被主动取消");
ReloadCompFileList(true);
return;
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"删除资源失败({ModEntity.path}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
isSuccessful = false;
}
// 取消选中
selectedMods.Remove(ModEntity.RawPath);
// 更改 Loader 和 UI 中的列表
ModLocalComp.compResourceListLoader.output.Remove(ModEntity);
searchResult?.Remove(ModEntity);
modItems.Remove(ModEntity.RawPath);
var indexOfUi = PanList.Children.IndexOf(PanList.Children.OfType<MyLocalCompItem>()
.FirstOrDefault(i => i.Entry.Equals(ModEntity)));
if (indexOfUi >= 0)
PanList.Children.RemoveAt(indexOfUi);
}
RefreshBars();
if (!isSuccessful)
{
HintService.Hint(Lang.Text("Instance.Resource.Delete.Failed"), HintType.Error);
ReloadCompFileList(true);
}
else if (PanList.Children.Count == 0)
{
ReloadCompFileList(true); // 删除了全部项目
}
else
{
RefreshBars();
}
// 显示结果提示
if (!isSuccessful)
return;
if (isShiftPressed)
{
if (modList.Count() == 1)
HintService.Hint(Lang.Text("Instance.Resource.Delete.PermanentSingle", modList.Single().FileName), HintType.Success);
else
HintService.Hint(Lang.Text("Instance.Resource.Delete.PermanentMultiple", modList.Count()), HintType.Success);
}
else if (modList.Count() == 1)
{
HintService.Hint(Lang.Text("Instance.Resource.Delete.RecycleSingle", modList.Single().FileName), HintType.Success);
}
else
{
HintService.Hint(Lang.Text("Instance.Resource.Delete.RecycleMultiple", modList.Count()), HintType.Success);
}
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "删除资源被主动取消");
ReloadCompFileList(true);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"删除资源出现未知错误",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
ReloadCompFileList(true);
}
LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly);
}
// 取消选择
private void BtnSelectCancel_Click(object sender, ModBase.RouteEventArgs e)
{
ChangeAllSelected(false);
}
// 收藏
private void BtnSelectFavorites_Click(object sender, ModBase.RouteEventArgs e)
{
var selected = ModLocalComp.compResourceListLoader.output
.Where(m => selectedMods.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp).ToList();
ModComp.CompFavorites.ShowMenu(selected, (UIElement)sender);
}
// 分享
private void BtnSelectShare_Click(object sender, ModBase.RouteEventArgs e)
{
var shareList = ModLocalComp.compResourceListLoader.output
.Where(m => selectedMods.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp.Id).ToHashSet();
ModBase.ClipboardSet(ModComp.CompFavorites.GetShareCode(shareList));
ChangeAllSelected(false);
}
#endregion
#region
// 详情
public void Info_Click(object sender, EventArgs e)
{
try
{
var modEntry = ((MyLocalCompItem)(sender is MyIconButton iconButton ? iconButton.Tag : sender)).Entry;
// 判断该 LabyMod 是否支持安装 Fabric Mod
var moddedLabyMod = PageInstanceLeft.McInstance.Info.HasLabyMod && PageInstanceLeft.McInstance.Modable;
// 加载失败信息
if (modEntry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable)
{
ModMain.MyMsgBox(
Lang.Text("Instance.Resource.Item.Info.FailedMessage.WithDetail",
modEntry.FileUnavailableReason.ToString()),
Lang.Text("Instance.Resource.Item.Info.FailedTitle"));
return;
}
if (modEntry.Comp is not null)
{
// 跳转到 Mod 下载页面
ModMain.frmMain.PageChange(new FormMain.PageStackData
{
page = FormMain.PageType.CompDetail,
additional = (modEntry.Comp, new List<string>(), PageInstanceLeft.McInstance.Info.VanillaName,
PageInstanceLeft.McInstance.Info.HasForge ? ModComp.CompLoaderType.Forge :
PageInstanceLeft.McInstance.Info.HasNeoForge ? ModComp.CompLoaderType.NeoForge :
PageInstanceLeft.McInstance.Info.HasFabric || moddedLabyMod ? ModComp.CompLoaderType.Fabric :
ModComp.CompLoaderType.Any,
currentCompType, null)
});
}
else
{
// 对于原理图文件,使用异步加载避免UI卡顿
if (modEntry.path.EndsWithF(".litematic", true) || modEntry.path.EndsWithF(".schem", true) ||
modEntry.path.EndsWithF(".schematic", true) || modEntry.path.EndsWithF(".nbt", true))
{
ShowSchematicInfoAsync(modEntry);
return;
}
// 获取信息
var contentLines = new List<string>();
// 检查是否为文件夹
if (modEntry.IsFolder)
{
// 处理文件夹详情
var folderPath = modEntry.ActualPath;
if (Directory.Exists(folderPath))
{
var fileCount = 0;
try
{
// 根据当前资源类型计算文件数量
switch (currentCompType)
{
case ModComp.CompType.Schematic:
{
fileCount = new DirectoryInfo(folderPath)
.EnumerateFiles("*", SearchOption.AllDirectories).Where(f =>
ModLocalComp.LocalCompFile.IsCompFile(f.FullName,
ModComp.CompType.Schematic)).Count();
break;
}
case ModComp.CompType.Mod:
{
fileCount = new DirectoryInfo(folderPath)
.EnumerateFiles("*.jar", SearchOption.AllDirectories).Count();
break;
}
case ModComp.CompType.ResourcePack:
{
fileCount = new DirectoryInfo(folderPath)
.EnumerateFiles("*.zip", SearchOption.AllDirectories).Count();
break;
}
case ModComp.CompType.Shader:
{
fileCount = new DirectoryInfo(folderPath)
.EnumerateFiles("*.zip", SearchOption.AllDirectories).Count();
break;
}
default:
{
fileCount = new DirectoryInfo(folderPath)
.EnumerateFiles("*", SearchOption.AllDirectories).Count();
break;
}
}
}
catch (Exception ex)
{
fileCount = 0;
}
if (fileCount == 0)
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.EmptyFolder") + "\r\n");
else if (fileCount == 1)
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.ContainsOne") + "\r\n");
else
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.ContainsMany", fileCount) + "\r\n");
}
else
{
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.FolderNotFound") + "\r\n");
}
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Path", folderPath));
}
else
{
// 处理普通文件详情
if (modEntry.Description is not null)
contentLines.Add(modEntry.Description + "\r\n");
if (modEntry.Authors is not null)
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Author", modEntry.Authors));
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.File", modEntry.FileName, ModBase.GetString(GetModFileInfo(modEntry.path).Length)));
if (modEntry.Version is not null)
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Version", modEntry.Version));
// 原理图文件的详情信息已通过异步方法处理
}
// 只有普通文件才显示调试信息
if (!modEntry.IsFolder)
{
var debugInfo = new List<string>();
if (modEntry.ModId is not null) debugInfo.Add(Lang.Text("Instance.Resource.Item.Info.ModId", modEntry.ModId));
if (modEntry.Dependencies.Any())
{
debugInfo.Add(Lang.Text("Instance.Resource.Item.Info.Dependency"));
foreach (var Dep in modEntry.Dependencies)
debugInfo.Add(" - " + (Dep.Value is null
? Dep.Key
: Lang.Text("Instance.Resource.Item.Info.DependencyVersion", Dep.Key, Dep.Value)));
}
if (debugInfo.Any())
{
contentLines.Add("");
contentLines.AddRange(debugInfo);
}
}
// 显示详情信息
if (modEntry.IsFolder)
{
// 文件夹只显示基本信息,不提供搜索功能
ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.Return"));
}
else
{
// 获取用于搜索的 Mod 名称
var modOriginalName = modEntry.Name.Replace(" ", "+");
var modSearchName = modOriginalName.Substring(0, 1);
for (int i = 1, loopTo = modOriginalName.Count() - 1; i <= loopTo; i++)
{
var isLastLower = modOriginalName[i - 1].ToString().ToLower()
.Equals(modOriginalName[i - 1].ToString());
var isCurrentLower = modOriginalName[i].ToString().ToLower()
.Equals(modOriginalName[i].ToString());
if (isLastLower && !isCurrentLower)
// 上一个字母为小写,这一个字母为大写
modSearchName += "+";
modSearchName += modOriginalName[i].ToString();
}
modSearchName = modSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine");
// 显示
if (currentCompType == ModComp.CompType.Schematic || !Lang.IsChineseMainland)
{
// 投影原理图文件或非中文区域不显示百科搜索选项
if (modEntry.Url is null)
ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.Return"));
else if (ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.OpenWebsite"), Lang.Text("Instance.Resource.Item.Info.Return")) ==
1) ModBase.OpenWebsite(modEntry.Url);
}
// 其他资源类型保留百科搜索功能
else if (modEntry.Url is null)
{
if (ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.McMod"), Lang.Text("Instance.Resource.Item.Info.Return")) == 1)
ModBase.OpenWebsite("https://www.mcmod.cn/s?key=" + modSearchName + "&site=all&filter=0");
}
else
{
switch (ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.OpenWebsite"), Lang.Text("Instance.Resource.Item.Info.McMod"),
Lang.Text("Instance.Resource.Item.Info.Return")))
{
case 1:
{
ModBase.OpenWebsite(modEntry.Url);
break;
}
case 2:
{
ModBase.OpenWebsite(
"https://www.mcmod.cn/s?key=" + modSearchName + "&site=all&filter=0");
break;
}
}
}
}
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"获取资源详情失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
// 打开文件所在的位置
public void Open_Click(MyIconButton sender, EventArgs e)
{
try
{
var listItem = (MyLocalCompItem)sender.Tag;
// 对于文件夹使用实际路径,对于文件使用原路径
var targetPath = listItem.Entry.IsFolder ? listItem.Entry.ActualPath : listItem.Entry.path;
ModBase.OpenExplorer(targetPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"打开资源文件位置失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
}
// 删除
public void Delete_Click(MyIconButton sender, EventArgs e)
{
var listItem = (MyLocalCompItem)sender.Tag;
DeleteMods(new[] { listItem.Entry });
}
// 启用 / 禁用
public void ED_Click(MyIconButton sender, EventArgs e)
{
var listItem = (MyLocalCompItem)sender.Tag;
EDMods(new[] { listItem.Entry }, listItem.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled);
}
/// <summary>
/// 异步显示原理图详情信息,避免UI卡顿
/// </summary>
private void ShowSchematicInfoAsync(ModLocalComp.LocalCompFile modEntry)
{
// 显示加载提示
HintService.Hint(Lang.Text("Instance.Resource.Item.Info.LoadingDetail"));
// 在后台线程中加载NBT数据
// 确保 NBT 数据已加载
// 在 UI 线程中显示详情
// 构建详情信息
// 根据文件类型显示详细信息
// 显示调试信息
// 显示详情对话框
// 记录错误日志但不显示错误提示,因为通用的文件状态检查已经处理了
ModBase.RunInNewThread(() =>
{
try
{
modEntry.LoadNbtDataIfNeeded();
ModBase.RunInUi(() =>
{
try
{
var contentLines = new List<string>();
if (modEntry.Description is not null) contentLines.Add(modEntry.Description + "\r\n");
if (modEntry.Authors is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Author", modEntry.Authors));
contentLines.Add(Lang.Text("Instance.Resource.Item.Info.File", modEntry.FileName, ModBase.GetString(GetModFileInfo(modEntry.path).Length)));
if (modEntry.Version is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Version", modEntry.Version));
if (modEntry.path.EndsWithF(".litematic", true))
ShowLitematicDetails(contentLines, modEntry);
else if (modEntry.path.EndsWithF(".schem", true))
ShowSchemDetails(contentLines, modEntry);
else if (modEntry.path.EndsWithF(".schematic", true))
ShowSchematicDetails(contentLines, modEntry);
else if (modEntry.path.EndsWithF(".nbt", true)) ShowNbtDetails(contentLines, modEntry);
ShowDebugInfo(contentLines, modEntry);
ShowSchematicDialog(contentLines, modEntry);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"显示原理图详情失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
});
}
catch (Exception ex)
{
ModBase.Log(
ex,
"加载原理图 NBT 数据失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Resource.Error.OperationFailed"));
}
});
}
#region
/// <summary>
/// 显示 Litematic 文件的详细信息
/// </summary>
private void ShowLitematicDetails(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
contentLines.Add("");
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DetailInfo"));
// 显示原始名称(从 NBT Metadata/Name 读取)
if (modEntry.LitematicOriginalName is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.OriginalName") + modEntry.LitematicOriginalName);
// 显示版本信息
if (modEntry.LitematicVersion.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.Version") + modEntry.LitematicVersion.Value);
// 显示尺寸信息
if (modEntry.LitematicEnclosingSize is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.EnclosingSize") + modEntry.LitematicEnclosingSize);
// 显示方块和体积统计
if (modEntry.LitematicTotalBlocks.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalBlocks") + Lang.Number(modEntry.LitematicTotalBlocks.Value, "N0"));
if (modEntry.LitematicTotalVolume.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalVolume") + Lang.Number(modEntry.LitematicTotalVolume.Value, "N0"));
// 显示区域数量
if (modEntry.LitematicRegionCount.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.RegionCount") + modEntry.LitematicRegionCount.Value);
// 显示时间信息
if (modEntry.LitematicTimeCreated.HasValue)
try
{
var createdTime = DateTimeOffset.FromUnixTimeMilliseconds(modEntry.LitematicTimeCreated.Value)
.ToLocalTime().DateTime;
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.CreatedTime") + Lang.Date(createdTime, "G"));
}
catch
{
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.CreatedTime") + modEntry.LitematicTimeCreated.Value);
}
if (modEntry.LitematicTimeModified.HasValue)
try
{
var modifiedTime = DateTimeOffset.FromUnixTimeMilliseconds(modEntry.LitematicTimeModified.Value)
.ToLocalTime().DateTime;
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.ModifiedTime") + Lang.Date(modifiedTime, "G"));
}
catch
{
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.ModifiedTime") + modEntry.LitematicTimeModified.Value);
}
}
/// <summary>
/// 显示 Schem 文件的详细信息
/// </summary>
private void ShowSchemDetails(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
contentLines.Add("");
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DetailInfo"));
// 显示原始名称(从 NBT Metadata/Name 读取)
if (modEntry.SchemOriginalName is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.OriginalName") + modEntry.SchemOriginalName);
// 显示版本信息
if (modEntry.StructureGameVersion is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.GameVersion") + modEntry.StructureGameVersion);
if (modEntry.SpongeVersion.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.SpongeVersion") + modEntry.SpongeVersion.Value);
if (modEntry.StructureDataVersion.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DataVersion") + modEntry.StructureDataVersion.Value);
// 显示尺寸信息
if (modEntry.LitematicEnclosingSize is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.EnclosingDimensions") + modEntry.LitematicEnclosingSize);
// 显示方块和体积统计
if (modEntry.LitematicTotalBlocks.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalBlocks") + Lang.Number(modEntry.LitematicTotalBlocks.Value, "N0"));
if (modEntry.LitematicTotalVolume.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalVolume") + Lang.Number(modEntry.LitematicTotalVolume.Value, "N0"));
// 显示区域数量
if (modEntry.LitematicRegionCount.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.RegionCount") + modEntry.LitematicRegionCount.Value);
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.FileType",
Lang.Text("Instance.Resource.Item.Schematic.FileType.Sponge")));
}
/// <summary>
/// 显示 Schematic 文件的详细信息
/// </summary>
private void ShowSchematicDetails(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
contentLines.Add("");
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DetailInfo"));
// 显示尺寸信息
if (modEntry.LitematicEnclosingSize is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.Size") + modEntry.LitematicEnclosingSize);
// 显示方块和体积统计
if (modEntry.LitematicTotalBlocks.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalBlocks") + Lang.Number(modEntry.LitematicTotalBlocks.Value, "N0"));
if (modEntry.LitematicTotalVolume.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalVolume") + Lang.Number(modEntry.LitematicTotalVolume.Value, "N0"));
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.FileType",
Lang.Text("Instance.Resource.Item.Schematic.FileType.Mcedit")));
}
/// <summary>
/// 显示 NBT 结构文件的详细信息
/// </summary>
private void ShowNbtDetails(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
contentLines.Add("");
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DetailInfo"));
// 显示作者信息
if (modEntry.StructureAuthor is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Info.Author", modEntry.StructureAuthor));
// 显示版本信息
if (modEntry.StructureGameVersion is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.GameVersion") + modEntry.StructureGameVersion);
if (modEntry.StructureDataVersion.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.DataVersion") + modEntry.StructureDataVersion.Value);
// 显示尺寸信息
if (modEntry.LitematicEnclosingSize is not null) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.EnclosingDimensions") + modEntry.LitematicEnclosingSize);
// 显示方块和体积统计
if (modEntry.LitematicTotalBlocks.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalBlocks") + Lang.Number(modEntry.LitematicTotalBlocks.Value, "N0"));
if (modEntry.LitematicTotalVolume.HasValue)
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.TotalVolume") + Lang.Number(modEntry.LitematicTotalVolume.Value, "N0"));
// 显示区域数量
if (modEntry.LitematicRegionCount.HasValue) contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.RegionCount") + modEntry.LitematicRegionCount.Value);
contentLines.Add(Lang.Text("Instance.Resource.Item.Schematic.FileType",
Lang.Text("Instance.Resource.Item.Schematic.FileType.Nbt")));
}
#endregion
private void ShowDebugInfo(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
var debugInfo = new List<string>();
if (modEntry.ModId is not null) debugInfo.Add(Lang.Text("Instance.Resource.Item.Info.ModId", modEntry.ModId));
if (modEntry.Dependencies.Any())
{
debugInfo.Add(Lang.Text("Instance.Resource.Item.Info.Dependency"));
foreach (var Dep in modEntry.Dependencies)
debugInfo.Add(" - " + Dep.Key + (Dep.Value is null
? Dep.Key
: Lang.Text("Instance.Resource.Item.Info.DependencyVersion", Dep.Key, Dep.Value)));
}
if (debugInfo.Any())
{
contentLines.Add("");
contentLines.AddRange(debugInfo);
}
}
private void ShowSchematicDialog(List<string> contentLines, ModLocalComp.LocalCompFile modEntry)
{
// 投影原理图文件不显示百科搜索选项
if (modEntry.Url is null)
ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.Return"));
else if (ModMain.MyMsgBox(contentLines.Join("\r\n"), modEntry.Name, Lang.Text("Instance.Resource.Item.Info.OpenWebsite"), Lang.Text("Instance.Resource.Item.Info.Return")) == 1)
ModBase.OpenWebsite(modEntry.Url);
}
#endregion
#region
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text);
private List<ModLocalComp.LocalCompFile> searchResult;
private CancellationTokenSource _cancelToken;
public void SearchRun(object sender, EventArgs e)
{
var curToken = new CancellationTokenSource();
var oldToken = Interlocked.Exchange(ref _cancelToken, curToken);
oldToken?.Cancel();
oldToken?.Dispose();
// this exception is ignored
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
try
{
var token = curToken.Token;
await Task.Delay(350, token);
if (token.IsCancellationRequested) return;
if (IsSearching)
{
var searchText = SearchBox.Text;
searchResult = await Task.Run(() => GetSearchResult(searchText), token);
}
if (token.IsCancellationRequested) return;
RefreshUI();
}
catch (TaskCanceledException)
{
}
catch (Exception ex)
{
ModBase.Log(ex, "搜索过程中发生异常");
}
}));
}
private List<ModLocalComp.LocalCompFile> GetSearchResult(string query)
{
// 构造请求
var queryList = new List<ModBase.SearchEntry<ModLocalComp.LocalCompFile>>();
foreach (var Entry in ModLocalComp.compResourceListLoader.output.AsReadOnly())
{
var searchSource = new List<ModBase.SearchSource>();
searchSource.Add(new ModBase.SearchSource(Entry.Name, 1d));
searchSource.Add(new ModBase.SearchSource(Entry.FileName, 1d));
if (Entry.Version is not null) searchSource.Add(new ModBase.SearchSource(Entry.Version, 0.2d));
if (Entry.Description is not null && !string.IsNullOrEmpty(Entry.Description))
searchSource.Add(new ModBase.SearchSource(Entry.Description, 0.4d));
if (Entry.Comp is not null)
{
if ((Entry.Comp.RawName ?? "") != (Entry.Name ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.RawName, 1d));
if ((Entry.Comp.TranslatedName ?? "") != (Entry.Comp.RawName ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.TranslatedName, 1d));
if ((Entry.Comp.Description ?? "") != (Entry.Description ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.Description, 0.4d));
searchSource.Add(new ModBase.SearchSource(string.Join("", Entry.Comp.Tags), 0.2d));
}
queryList.Add(new ModBase.SearchEntry<ModLocalComp.LocalCompFile>
{ item = Entry, searchSource = searchSource });
}
// 进行搜索
return ModBase.Search(queryList, query, ModBase.MaxLocalSearchDepth, 0.35d).Select(r => r.item).ToList();
}
#endregion
}
@@ -0,0 +1,292 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceExport"
PanScroll="{Binding ElementName=PanBack}">
<Grid x:Name="PanAllBack" AllowDrop="True">
<Grid.Resources>
<Style TargetType="local:MyCheckBox">
<Setter Property="Height" Value="26" />
</Style>
</Grid.Resources>
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
x:Name="PanBack">
<StackPanel Margin="25,10,25,65">
<local:MyHint Margin="0,15,0,0" Text="{DynamicResource Instance.Export.ModrinthWarning}" Theme="Yellow"
x:Name="HintOptiFine" />
<local:MyCard Margin="0,15">
<Grid Margin="22,15,25,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="0.3*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Export.PackName}" Margin="0,0,20,0" />
<local:MyTextBox Grid.Row="0" Grid.Column="1" x:Name="TextExportName" MaxLength="100" />
<TextBlock Grid.Row="0" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Export.PackVersion}" Margin="30,0,20,0" />
<local:MyTextBox Grid.Row="0" Grid.Column="3" x:Name="TextExportVersion"
Tag="VersionArgumentTitle" MaxLength="100" />
</Grid>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" x:Name="CardOptions" MinHeight="39">
<local:MyIconTextButton x:Name="BtnOverrideCancel" Height="24" HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,5,0"
IsHitTestVisible="False" Opacity="0.5"
Text="{DynamicResource Common.Action.Reset}"
SvgIcon="lucide/rotate-ccw"
LogoScale="0.9" />
<StackPanel Margin="25,38,15,15" Name="PanOptions">
<local:MyCheckBox IsEnabled="False" x:Name="CheckOptionsBasic">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Game}"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox x:Name="CheckOptionsOptions" Margin="30,0,0,0">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.GameSettings}"
Description="{DynamicResource Instance.Export.Option.GameSettings.Desc}"
Rules="options.txt|configureddefaults/"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox Margin="30,0,0,0">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.GameProfile}"
Description="{DynamicResource Instance.Export.Option.GameProfile.Desc}"
Rules="hotbar.nbt|command_history.txt"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox Margin="30,0,0,0">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.OptiFine}"
Rules="optionsof.txt|optionsshaders.txt"
DefaultChecked="True"
RequireOptiFine="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox x:Name="CheckOptionsMod">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Mod}"
Description="{DynamicResource Instance.Export.Option.Mod.Desc}"
Rules="mods/|!mods/*.disabled|!mods/*.old|!mods/.connector/|coremods/|lib/|!mods/mcef-libraries/|!mods/mcef-cache/"
DefaultChecked="True"
RequireModLoader="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0"
Visibility="{Binding Checked, ElementName=CheckOptionsMod, Converter={StaticResource BooleanToVisibilityConverter}}">
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.DisabledMod}"
Rules="mods/*.disabled|mods/*.old" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.ImportantData}"
Description="{DynamicResource Instance.Export.Option.ImportantData.Desc}"
Rules="addons/|multiblocked/|modpack-update-checker/|global_packs/|global_resource_packs/|global_data_packs/|optional_data_packs/|maps/|icon.png|mods-resourcepacks/|matmos/|resource_assorts/|resource_assorts.json|patchouli_books/|datapacks/|kubejs*/|!kubejs*/probe/|!kubejs*/exported/|!kubejs*/jsconfig.json|!kubejs*/README.txt|openloader/|worldshape/|resources/|scripts/|structures/|fontfiles/|oresources/|packmenu/|craftpresence/|pointblanks/|template*/|!template*/playerdata/|!template*/stats/"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.ModSettings}"
Rules="config/|!config/accountsx/|!config/jei/world/|!config/worldedit/|config/worldedit/worldedit.properties|!config/spark/|config/spark/config.json|defaultconfigs/|journeymap/config/|journeymap/server/|TrashSlotSaveState.json|customfov.txt|gg.essential.mod/|essential/|!essential/*/|!essential/*.jar*|!essential/screenshot-checksum-caches.json|!essential/microsoft_accounts.json|paragliderSettings.nbt|local/client_config.json|local/ftbl.json|local/client/sidebar_buttons.json|local/client/ftbutilities.cfg|local/client/ftblib.cfg|local/client/xencraft.cfg|liteloader.properties|default_reference.xml|CustomSkinLoader/CustomSkinLoader.json"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Maps}"
Description="{DynamicResource Instance.Export.Option.Maps.Desc}"
Rules="journeymap/data/|xaero/|XaeroWaypoints/|XaeroWorldMap/"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Jei}"
Description="{DynamicResource Instance.Export.Option.Jei.Desc}"
Rules="config/jei/world/"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Emi}"
Description="{DynamicResource Instance.Export.Option.Emi.Desc}"
Rules="emi.json"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Patchouli}"
Description="{DynamicResource Instance.Export.Option.Patchouli.Desc}"
Rules="patchouli_data.json"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
</StackPanel>
<local:MyCheckBox x:Name="CheckOptionsResourcePacks">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.ResourcePacks}"
Description="{DynamicResource Instance.Export.Option.ResourcePacks.Desc}"
ShowRules="resourcepacks/|texturepacks/"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0" x:Name="PanOptionsResourcePacks"
Visibility="{Binding Checked, ElementName=CheckOptionsResourcePacks, Converter={StaticResource BooleanToVisibilityConverter}}" />
<local:MyCheckBox x:Name="CheckOptionsShaderPacks">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.ShaderPacks}"
ShowRules="shaderpacks/"
DefaultChecked="True"
RequireModLoaderOrOptiFine="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0" x:Name="PanOptionsShaderPacks"
Visibility="{Binding Checked, ElementName=CheckOptionsShaderPacks, Converter={StaticResource BooleanToVisibilityConverter}}" />
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Screenshots}"
Rules="screenshots/"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Structures}"
Description="{DynamicResource Instance.Export.Option.Structures.Desc}"
Rules="schematics/"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Replay}"
Description="{DynamicResource Instance.Export.Option.Replay.Desc}"
Rules="replay_recordings/|replay_videos/"
RequireModLoader="True"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox x:Name="CheckOptionsSaves">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Saves}"
Description="{DynamicResource Instance.Export.Option.Saves.Desc}"
ShowRules="saves/"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0" x:Name="PanOptionsSaves"
Visibility="{Binding Checked, ElementName=CheckOptionsSaves, Converter={StaticResource BooleanToVisibilityConverter}}" />
<local:MyCheckBox>
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Servers}"
Rules="servers.dat"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<local:MyCheckBox x:Name="CheckOptionsOtherFolders" Change="CheckOptionsOtherFolders_Change">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.OtherFolders}"
Description="{DynamicResource Instance.Export.Option.OtherFolders.Desc}"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0" x:Name="PanOptionsOtherFolders"
Visibility="{Binding Checked, ElementName=CheckOptionsOtherFolders, Converter={StaticResource BooleanToVisibilityConverter}}" />
<local:MyCheckBox x:Name="CheckOptionsPcl">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.Launcher}"
Description="{DynamicResource Instance.Export.Option.Launcher.Desc}"
DefaultChecked="False" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
<StackPanel Margin="30,0,0,0"
Visibility="{Binding Checked, ElementName=CheckOptionsPcl, Converter={StaticResource BooleanToVisibilityConverter}}">
<local:MyCheckBox x:Name="CheckOptionsPclCustom">
<local:MyCheckBox.Tag>
<local:ExportOption
Title="{DynamicResource Instance.Export.Option.LauncherCustom}"
Description="{DynamicResource Instance.Export.Option.LauncherCustom.Desc}"
DefaultChecked="True" />
</local:MyCheckBox.Tag>
</local:MyCheckBox>
</StackPanel>
</StackPanel>
</local:MyCard>
<local:MyCard Title="{DynamicResource Instance.Export.Advanced.Title}" Margin="0,0,0,15" CanSwap="True"
IsSwapped="True">
<StackPanel Margin="25,37,23,20">
<local:MyHint
Text="{DynamicResource Instance.Export.Advanced.BundleHint}"
Theme="Yellow" Margin="0,2,0,8"
Visibility="{Binding Checked, ElementName=CheckAdvancedInclude, Converter={StaticResource BooleanToVisibilityConverter}}" />
<local:MyCheckBox Text="{DynamicResource Instance.Export.Advanced.BundleFiles}"
x:Name="CheckAdvancedInclude"
Change="CheckAdvancedInclude_Change"
ToolTip="{DynamicResource Instance.Export.Advanced.BundleFiles.ToolTip}" />
<local:MyCheckBox Text="{DynamicResource Instance.Export.Advanced.Modrinth}"
x:Name="CheckAdvancedModrinth"
Change="CheckAdvancedModrinth_Change"
ToolTip="{DynamicResource Instance.Export.Advanced.Modrinth.ToolTip}" />
<local:MyHint Theme="Blue" RelativeSetup="HintExportConfig" CanClose="True" Margin="0,20,0,2"
Text="{DynamicResource Instance.Export.Advanced.ConfigHint}" />
<Grid Height="35" Margin="0,12,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton Grid.Column="0" x:Name="BtnAdvancedImport" MinWidth="140"
Text="{DynamicResource Instance.Export.Advanced.ImportConfig}"
Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" />
<local:MyButton Grid.Column="1" x:Name="BtnAdvancedExport" MinWidth="140"
Text="{DynamicResource Instance.Export.Advanced.ExportConfig}"
Padding="13,0" Margin="0,0,20,0" />
</Grid>
</StackPanel>
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
<local:MyExtraTextButton HorizontalAlignment="Center" VerticalAlignment="Bottom"
x:Name="BtnExport" Text="{DynamicResource Instance.Export.StartExport}"
LogoScale="1.1"
SvgIcon="lucide/package" />
</Grid>
</local:MyPageRight>
@@ -0,0 +1,1226 @@
using System.IO;
using System.IO.Compression;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using DotNet.Globbing;
using PCL.Core.App;
using PCL.Core.UI;
using PCL.Core.App.Localization;
using PCL.Core.Utils;
namespace PCL;
public class ExportOption : DependencyObject
{
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
nameof(Title), typeof(string), typeof(ExportOption)
);
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
nameof(Description), typeof(string), typeof(ExportOption)
);
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public string Rules { get; set; }
/// <summary>
/// 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。
/// 如果 ShowRules 也为空,则始终显示。
/// </summary>
public string ShowRules { get; set; }
public bool DefaultChecked { get; set; }
public bool RequireModLoader { get; set; }
public bool RequireOptiFine { get; set; }
public bool RequireModLoaderOrOptiFine { get; set; }
}
public partial class PageInstanceExport : IRefreshable
{
private string currentVersion = "";
public PageInstanceExport()
{
InitializeComponent();
Loaded += (_, _) => PageInstanceExport_Loaded();
CardOptions.MouseLeftButtonDown += CardOptions_MouseLeftButtonDown;
BtnAdvancedExport.Click += ExportConfig;
BtnAdvancedImport.Click += ImportConfig;
BtnExport.Click += StartExport;
TextExportName.GotFocus += TextExportName_GotFocus;
CheckAdvancedModrinth.Change += CheckAdvancedModrinth_Change;
CheckAdvancedInclude.Change += CheckAdvancedInclude_Change;
}
void IRefreshable.Refresh()
{
RefreshAll();
}
private void PageInstanceExport_Loaded()
{
ModAnimation.AniControlEnabled += 1;
if ((currentVersion ?? "") != (PageInstanceLeft.McInstance.PathInstance ?? ""))
RefreshAll(); // 切换到了另一个实例,重置页面
ModAnimation.AniControlEnabled -= 1;
}
public void RefreshAll()
{
ModBase.Log("[Export] 刷新导出页面");
HintOptiFine.Visibility =
PageInstanceLeft.McInstance.Info.HasOptiFine ? Visibility.Visible : Visibility.Collapsed;
currentVersion = PageInstanceLeft.McInstance.PathInstance;
TextExportName.Text = "";
TextExportName.HintText = PageInstanceLeft.McInstance.Name;
TextExportVersion.Text = "";
TextExportVersion.HintText = "1.0.0";
CheckAdvancedInclude.Checked = false;
CheckAdvancedModrinth.Checked = false;
GetExportOption(CheckOptionsBasic).Description = PageInstanceLeft.McInstance.GetDefaultDescription();
ResetConfigOverrides();
ReloadAllSubOptions();
RefreshAllOptionsUI();
PanBack.ScrollToHome();
}
// 自动填写整合包名称
private void TextExportName_GotFocus(object sender, RoutedEventArgs routedEventArgs)
{
if (string.IsNullOrEmpty(TextExportName.Text))
{
TextExportName.Text = TextExportName.HintText;
TextExportName.SelectionStart = TextExportName.Text.Length;
}
}
// 勾选 Modrinth 上传模式时,禁止打包 PCL
private void CheckAdvancedModrinth_Change(object sender, bool user)
{
if (CheckAdvancedModrinth.Checked == true)
CheckOptionsPcl.Checked = false;
CheckOptionsPcl.IsEnabled = (bool)!CheckAdvancedModrinth.Checked;
}
// 勾选"其他文件夹"时,同步勾选/取消所有子选项
private void CheckOptionsOtherFolders_Change(object sender, bool user)
{
if (!user) return;
foreach (var child in PanOptionsOtherFolders.Children)
if (child is MyCheckBox childBox)
childBox.Checked = CheckOptionsOtherFolders.Checked;
}
// 勾选打包资源文件时,禁止开启 Modrinth 上传模式
private void CheckAdvancedInclude_Change(object sender, bool user)
{
if (CheckAdvancedInclude.Checked == true)
CheckAdvancedModrinth.Checked = false;
CheckAdvancedModrinth.IsEnabled = (bool)!CheckAdvancedInclude.Checked;
}
#region
private readonly string[] subOptionBlackList = new[] { "Quark Programmer Art.zip", "+ EuphoriaPatches_" };
/// <summary>
/// 动态生成子文件夹下的选项,例如资源包、存档等。
/// </summary>
private void ReloadAllSubOptions()
{
ReloadSubOptions(PanOptionsResourcePacks, true, true, "resourcepacks", "texturepacks");
ReloadSubOptions(PanOptionsSaves, false, true, "saves");
ReloadSubOptions(PanOptionsShaderPacks, true, true, "shaderpacks");
ReloadOtherFolders();
}
/// <summary>
/// 扫描实例根目录下未被已有选项覆盖的文件夹,生成独立的复选框。
/// </summary>
private void ReloadOtherFolders()
{
PanOptionsOtherFolders.Children.Clear();
var pathIndie = PageInstanceLeft.McInstance.PathIndie;
var rootDir = new DirectoryInfo(pathIndie);
if (!rootDir.Exists)
{
CheckOptionsOtherFolders.Visibility = Visibility.Collapsed;
return;
}
var coveredFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// 模组
"mods", "coremods", "lib",
// 整合包重要数据
"addons", "multiblocked", "modpack-update-checker", "global_packs",
"global_resource_packs", "global_data_packs", "optional_data_packs", "maps",
"mods-resourcepacks", "matmos", "resource_assorts",
"patchouli_books", "datapacks",
"openloader", "worldshape", "resources", "scripts", "structures",
"fontfiles", "oresources", "packmenu", "craftpresence", "pointblanks",
// 模组设置
"config", "defaultconfigs", "journeymap", "local", "essential", "gg.essential.mod",
"CustomSkinLoader",
// 地图
"xaero", "XaeroWaypoints", "XaeroWorldMap",
// 资源包
"resourcepacks", "texturepacks",
// 光影
"shaderpacks",
// 截图 / 结构 / 录像
"screenshots", "schematics",
"replay_recordings", "replay_videos",
// 存档 / 设置文件夹
"saves", "configureddefaults",
// 始终跳过(大量文件或无用缓存)
"assets", "versions", "libraries", "structureCacheV1",
".fabric", ".git", "avatar-cache", "cosmetic-cache",
// PCL 单独处理
"PCL",
};
var coveredPrefixes = new[] { "kubejs", "template" };
var coveredSuffixes = new[] { "-natives" };
foreach (var subDir in rootDir.EnumerateDirectories())
{
if (coveredFolders.Contains(subDir.Name))
continue;
if (coveredPrefixes.Any(p => subDir.Name.StartsWithF(p)))
continue;
if (coveredSuffixes.Any(s => subDir.Name.EndsWithF(s, true)))
continue;
PanOptionsOtherFolders.Children.Add(new MyCheckBox
{
Tag = new ExportOption
{
Title = subDir.Name,
DefaultChecked = false,
Rules = ModBase.EscapeLikePattern($"{subDir.Name}/")
}
});
}
CheckOptionsOtherFolders.Visibility = PanOptionsOtherFolders.Children.Count > 0
? Visibility.Visible
: Visibility.Collapsed;
}
private void ReloadSubOptions(StackPanel panel, bool acceptCompressedFile, bool acceptFolder,
params string[] folders)
{
panel.Children.Clear();
foreach (var Folder in folders)
{
var targetFolder = new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie + Folder);
if (!targetFolder.Exists)
continue;
// 查找文件夹下的对应项
if (acceptCompressedFile)
foreach (var File in targetFolder.EnumerateFiles("*.zip").Concat(targetFolder.EnumerateFiles("*.rar")))
{
if (subOptionBlackList.Any(b => File.Name.ContainsF(b)))
continue;
panel.Children.Add(new MyCheckBox
{
Tag = new ExportOption
{
Title = File.Name, DefaultChecked = true,
Rules = ModBase.EscapeLikePattern($"{Folder}/{File.Name}")
}
});
if (Folder == "shaderpacks") // 处理光影包的配置文件
{
var shaderConfig = new FileInfo(Path.Combine(File.Directory.FullName,
$"{File.Name}.txt"));
if (shaderConfig.Exists)
panel.Children.Add(new MyCheckBox
{
Margin = new Thickness(30, 0, 0, 0),
Tag = new ExportOption
{
Title = $"{shaderConfig.Name}", DefaultChecked = true,
Description = Lang.Text("Instance.Export.Config.ShaderConfigSuffix"),
Rules = ModBase.EscapeLikePattern($"{Folder}/{shaderConfig.Name}")
}
});
}
}
if (acceptFolder)
foreach (var SubFolder in targetFolder.EnumerateDirectories().OrderByDescending(f => f.LastWriteTime))
{
if (subOptionBlackList.Any(b => SubFolder.Name.ContainsF(b)))
continue;
if (!SubFolder.EnumerateFileSystemInfos().Any())
continue;
var newCheckBox = new MyCheckBox
{
Tag = new ExportOption
{
Title = SubFolder.Name, DefaultChecked = true,
Rules = ModBase.EscapeLikePattern($"{Folder}/{SubFolder.Name}/")
}
};
if (ReferenceEquals(panel, PanOptionsSaves))
GetExportOption(newCheckBox).Description =
Lang.Date(SubFolder.LastWriteTime, "g");
panel.Children.Add(newCheckBox);
if (Folder == "shaderpacks") // 处理文件夹形式光影包的配置文件
{
var shaderConfig = new FileInfo(Path.Combine(targetFolder.FullName,
$"{SubFolder.Name}.txt"));
if (shaderConfig.Exists)
panel.Children.Add(new MyCheckBox
{
Margin = new Thickness(30, 0, 0, 0),
Tag = new ExportOption
{
Title = $"{shaderConfig.Name}", DefaultChecked = true,
Description = Lang.Text("Instance.Export.Config.ShaderConfigSuffix"),
Rules = ModBase.EscapeLikePattern($"{Folder}/{shaderConfig.Name}")
}
});
}
}
}
}
#endregion
#region
/// <summary>
/// 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。
/// </summary>
private void RefreshAllOptionsUI()
{
// 预先归纳所有至多二级的文件/文件夹
var allEntries = new List<string>();
bool IsValidDirectory(DirectoryInfo folder)
{
try
{
return folder.Exists && folder.EnumerateFileSystemInfos()
.Any(i => !subOptionBlackList.Any(b => i.Name.ContainsF(b)));
}
catch
{
return false;
}
}
; // 检查文件夹不为空
// 一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接)
var pathInfo = new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie);
allEntries.AddRange(pathInfo.EnumerateFiles().Select(f => f.Name));
foreach (var SubFolder in pathInfo.EnumerateDirectories().Where(IsValidDirectory))
{
allEntries.Add($@"{SubFolder.Name}\");
allEntries.AddRange(SubFolder.EnumerateFiles().Select(f => $@"{SubFolder.Name}\{f.Name}"));
allEntries.AddRange(SubFolder.EnumerateDirectories().Where(IsValidDirectory)
.Select(d => $@"{SubFolder.Name}\{d.Name}\"));
}
ModBase.Log($"[Export] 共发现 {allEntries.Count} 个可行的二级文件/文件夹");
// 确认选项是否应该被显示
bool IsVisible(ExportOption targetOption)
{
// 检查需要 OptiFine 或 Mod 加载器
if (targetOption.RequireOptiFine && !PageInstanceLeft.McInstance.Info.HasOptiFine)
return false;
if (targetOption.RequireModLoader && !PageInstanceLeft.McInstance.Modable)
return false;
if (targetOption.RequireModLoaderOrOptiFine && !PageInstanceLeft.McInstance.Info.HasOptiFine &&
!PageInstanceLeft.McInstance.Modable)
return false;
// 粗略检查是否可能有符合规则的文件/文件夹
return StandardizeLines((targetOption.Rules ?? targetOption.ShowRules).Split('|'), true).Any(rule =>
{
if (rule.StartsWithF("!"))
return false; // 只看正向规则
// 检查前两级
try
{
if (allEntries.Any(entry => LikeString(entry, rule)))
return true;
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"错误的规则:{rule}",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Export.Error.OperationFailed"));
return false;
}
// 粗略检查所有级
rule = rule.Trim("*?".ToCharArray());
if (rule.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries).Count() >= 3)
{
if (rule.EndsWithF(@"\"))
return IsValidDirectory(new DirectoryInfo(PageInstanceLeft.McInstance.PathIndie + rule)); // 文件夹有效
return File.Exists(PageInstanceLeft.McInstance.PathIndie + rule);
// 文件有效
}
return false;
});
}
;
// 逐个检查选项
foreach (var CheckBox in GetAllOptions(true))
{
var targetOption = GetExportOption(CheckBox);
// 名称与简介
CheckBox.Inlines.Clear();
CheckBox.Inlines.Add(new Run(targetOption.Title));
if (!string.IsNullOrEmpty(targetOption.Description))
CheckBox.Inlines.Add(new Run(" " + targetOption.Description) { Foreground = ThemeManager.colorGray5 });
// 可见性、默认勾选
if (string.IsNullOrEmpty(targetOption.Rules) && string.IsNullOrEmpty(targetOption.ShowRules))
{
CheckBox.Visibility = Visibility.Visible;
CheckBox.Checked = targetOption.DefaultChecked;
}
else
{
var pass = IsVisible(targetOption);
CheckBox.Visibility = pass ? Visibility.Visible : Visibility.Collapsed;
CheckBox.Checked = targetOption.DefaultChecked && pass;
}
}
}
/// <summary>
/// 对文本行进行标准化处理,以便使用 Like 进行匹配。
/// </summary>
private IEnumerable<string> StandardizeLines(IEnumerable<string> raw, bool addSuffixStarToFolderPath)
{
foreach (var IgnoreLineRaw in raw)
{
var ignoreLine = IgnoreLineRaw;
ignoreLine = ignoreLine.Trim();
if (string.IsNullOrEmpty(ignoreLine) || ignoreLine.StartsWithF("#") || ignoreLine.StartsWithF("="))
continue;
ignoreLine = ignoreLine.Replace("/", @"\");
yield return ignoreLine + (ignoreLine.EndsWithF(@"\") && addSuffixStarToFolderPath ? "**" : "");
}
}
/// <summary>
/// 获取所有可作为选项的 CheckBox。
/// </summary>
private IEnumerable<MyCheckBox> GetAllOptions(bool includeHidden)
{
foreach (var Element in PanOptions.Children)
{
if (!includeHidden &&
((UIElement)Element).Visibility != Visibility.Visible)
continue;
if (Element is MyCheckBox)
yield return (MyCheckBox)Element;
else if (Element is StackPanel)
foreach (var SubElement in ((StackPanel)Element).Children)
{
if (!includeHidden && ((UIElement)SubElement).Visibility != Visibility.Visible)
continue;
if (SubElement is MyCheckBox)
yield return (MyCheckBox)SubElement;
}
}
}
/// <summary>
/// 获取该 CheckBox 对应的 ExportOption。
/// </summary>
private ExportOption GetExportOption(MyCheckBox checkBox)
{
return (ExportOption)checkBox.Tag;
}
#endregion
#region
private const string sperator = "==============================================================";
// ================ 导出内容段 ================
/// <summary>
/// 从配置文件中读取的规则。
/// 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。
/// </summary>
private List<string> RulesOverrides
{
get => field;
set
{
field = value;
if (value is null)
{
BtnOverrideCancel.Visibility = Visibility.Collapsed;
PanOptions.Visibility = Visibility.Visible;
CardOptions.Inlines.Clear();
CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionListTitle")) { FontWeight = FontWeights.Bold });
}
else
{
BtnOverrideCancel.Visibility = Visibility.Visible;
PanOptions.Visibility = Visibility.Collapsed;
CardOptions.Inlines.Clear();
CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionListTitle") + ":") { FontWeight = FontWeights.Bold });
CardOptions.Inlines.Add(new Run(Lang.Text("Instance.Export.OptionList.FromConfig")) { FontWeight = FontWeights.Normal });
}
}
}
/// <summary>
/// 获取当前实际生效的所有规则。
/// </summary>
private IEnumerable<string> GetAllRules()
{
if (RulesOverrides is not null)
{
// 返回覆盖的列表
foreach (var Rule in RulesOverrides)
yield return Rule;
}
else
{
// 从当前勾选的所有选项中获取所有规则行
yield return "";
yield return "# " + Lang.Text("Instance.Export.Config.Comment.ModifyRules");
yield return "# " + Lang.Text("Instance.Export.Config.Comment.ReverseMatch");
yield return "";
foreach (var CheckBox in GetAllOptions(false))
{
if (CheckBox.Checked == false)
continue;
var targetOption = GetExportOption(CheckBox);
if (targetOption.Rules is null)
continue;
yield return $"# {targetOption.Title}";
foreach (var Rule in targetOption.Rules.Split('|'))
yield return Rule;
yield return "";
}
yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExcludedFiles");
yield return "!*.log";
yield return "!*.dat_old";
yield return "!*.BakaCoreInfo";
yield return "!hmclversion.cfg";
yield return "!log4j2.xml";
yield return "";
}
}
// ================ 追加内容段 ================
private List<string> extraFiles;
/// <summary>
/// 获取当前实际生效的追加内容。
/// </summary>
private IEnumerable<string> GetExtraFileLines()
{
if (extraFiles is not null)
{
// 返回覆盖的列表
foreach (var File in extraFiles)
yield return File;
}
else
{
// 从当前勾选的所有选项中获取所有规则行
yield return "";
yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExtraFiles");
yield return "# " + Lang.Text("Instance.Export.Config.Comment.ExtraFiles2");
yield return "";
}
}
// ================ 重置 ================
/// <summary>
/// 重置配置文件所带来的影响。
/// </summary>
private void ResetConfigOverrides()
{
RulesOverrides = null;
configPackPath = null;
extraFiles = null;
PanBack.ScrollToHome();
}
private void CardOptions_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (RulesOverrides is null)
return;
ResetConfigOverrides();
}
// ================ 保存 / 读取 ================
// 保存配置文件
private void ExportConfig(object sender, MouseButtonEventArgs e)
{
try
{
var configPath = SystemDialogs.SelectSaveFile(Lang.Text("Instance.Export.SelectFileLocation"), "export_config.txt", Lang.Text("Instance.Export.Config.FileFilter"),
(string?)States.System.ExportConfigPath);
if (string.IsNullOrEmpty(configPath))
return;
States.System.ExportConfigPath = configPath;
var configLines = new List<string>();
// ini 段
configLines.Add("Name:" + TextExportName.Text);
configLines.Add("Version:" + TextExportVersion.Text);
configLines.Add("");
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.IncludeLauncher"));
configLines.Add("IncludeLauncher:" + CheckOptionsPcl.Checked);
configLines.Add("");
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.IncludeLauncherCustom"));
configLines.Add("IncludeLauncherCustom:" + CheckOptionsPclCustom.Checked);
configLines.Add("");
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles2"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.BundleFiles3"));
configLines.Add("DontCheckHostedAssets:" + CheckAdvancedInclude.Checked);
configLines.Add("");
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth2"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.Modrinth3"));
configLines.Add("ModrinthUploadMode:" + CheckAdvancedModrinth.Checked);
configLines.Add("");
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath2"));
configLines.Add("# " + Lang.Text("Instance.Export.Config.Comment.PackPath3"));
configLines.Add("PackPath:" + (configPackPath ?? ""));
configLines.Add("");
// 导出内容段
configLines.Add(sperator);
configLines.AddRange(GetAllRules());
// 追加内容段
configLines.Add(sperator);
configLines.AddRange(GetExtraFileLines());
// 结束
ModBase.WriteFile(configPath, configLines.Join("\r\n"));
HintService.Hint(Lang.Text("Instance.Export.SaveSuccess", configPath), HintType.Success);
ModBase.OpenExplorer(configPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"保存配置失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Export.Error.OperationFailed"));
}
}
#region
/// <summary>
/// 从指定路径读取配置文件(供按钮和拖放调用)
/// </summary>
/// <param name="configPath">配置文件路径</param>
private void ReadConfigFile(string configPath)
{
try
{
// 保存配置文件路径到缓存
States.System.ExportConfigPath = configPath;
var fileContent = ModBase.ReadFile(configPath);
var segments = fileContent.Split(sperator);
if (segments.Length == 0)
{
HintService.Hint(Lang.Text("Instance.Export.Config.Invalid"), HintType.Error);
return;
}
// === 解析INI段 ===
var ini = new Dictionary<string, string>();
foreach (var LineRaw in segments[0].Split("\r\n".ToCharArray()))
{
var line = LineRaw;
line = line.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWithF("#") || line.StartsWithF("="))
continue;
var index = line.IndexOfF(":");
if (index > 0) ini[line.Substring(0, index)] = line.Substring(index + 1);
}
// 赋值到界面控件
TextExportName.Text = ini.GetOrDefault("Name", "");
TextExportVersion.Text = ini.GetOrDefault("Version", "");
CheckOptionsPcl.Checked =
Convert.ToBoolean(ini.GetOrDefault("IncludeLauncher", false.ToString()));
CheckOptionsPclCustom.Checked =
Convert.ToBoolean(ini.GetOrDefault("IncludeLauncherCustom", true.ToString()));
CheckAdvancedModrinth.Checked =
Convert.ToBoolean(ini.GetOrDefault("ModrinthUploadMode", false.ToString()));
CheckAdvancedInclude.Checked =
Convert.ToBoolean(ini.GetOrDefault("DontCheckHostedAssets", false.ToString()));
configPackPath = ini.GetOrDefault("PackPath");
// === 解析导出内容段 ===
RulesOverrides = segments[1].Replace("\r", "\n")
.Replace("\n" + "\n", "\n").Split("\n").ToList();
// === 解析追加内容段 ===
if (segments.Length > 2)
extraFiles = segments[2].Replace("\r", "\n")
.Replace("\n" + "\n", "\n").Split("\n").ToList();
else
extraFiles = null;
// 提示成功
HintService.Hint(Lang.Text("Instance.Export.ReadSuccess", configPath), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"读取配置文件失败:{configPath}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Export.Error.OperationFailed"));
}
}
#endregion
// 读取配置文件
private void ImportConfig(object sender, MouseButtonEventArgs e)
{
try
{
var configPath = SystemDialogs.SelectFile(Lang.Text("Instance.Export.Config.FileFilter"), Lang.Text("Instance.Export.SelectConfigFile"),
(string?)States.System.ExportConfigPath);
if (string.IsNullOrEmpty(configPath))
return;
// 调用核心读取逻辑
ReadConfigFile(configPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"选择配置文件失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Export.Error.OperationFailed"));
}
}
#region
/// <summary>
/// 文件拖入界面时触发:验证文件类型
/// </summary>
private void PanAllBack_DragEnter(object sender, DragEventArgs e)
{
// 检查是否包含文件拖放数据
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
// 获取拖入的文件路径数组
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
// 验证:仅允许单个.txt文件
if (files.Length == 1 &&
files[0].EndsWithF(".txt", true))
e.Effects = DragDropEffects.Copy; // 设置拖放效果为“复制”
else
e.Effects = DragDropEffects.None; // 不允许拖放
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
}
/// <summary>
/// 文件放下时触发:读取配置文件
/// </summary>
private void PanAllBack_Drop(object sender, DragEventArgs e)
{
// 获取拖入的文件路径
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[])e.Data.GetData(DataFormats.FileDrop);
var configPath = files[0];
// 调用核心读取逻辑
ReadConfigFile(configPath);
}
e.Handled = true;
}
#endregion
#endregion
#region
/// <summary>
/// 配置文件中指定的导出位置。
/// </summary>
private string configPackPath;
/// <summary>
/// 开始导出。
/// </summary>
private void StartExport(object sender, MouseButtonEventArgs e)
{
var packName = string.IsNullOrEmpty(TextExportName.Text) ? TextExportName.HintText : TextExportName.Text;
var packVersion = string.IsNullOrEmpty(TextExportVersion.Text) ? "1.0.0" : TextExportVersion.Text;
// 重复任务检查
var loaderName = Lang.Text("Instance.Export.ExportTask.Prefix") + packName;
foreach (var OngoingLoader in ModLoader.loaderTaskbar)
{
if ((OngoingLoader.name ?? "") != (loaderName ?? ""))
continue;
ModMain.frmMain.PageChange(FormMain.PageType.TaskManager);
return;
}
// 确认导出位置
string packPath = null;
if (!string.IsNullOrWhiteSpace(configPackPath) && !configPackPath.EndsWithF(@"\") &&
!configPackPath.EndsWithF("/"))
try
{
Directory.CreateDirectory(ModBase.GetPathFromFullPath(configPackPath));
packPath = configPackPath;
ModBase.Log($"[Export] 使用配置文件中指定的导出路径:{configPackPath}");
}
catch (Exception ex)
{
ModBase.Log(ex, $"无法使用配置文件中指定的导出路径({configPackPath}");
if (ModMain.MyMsgBox(
Lang.Text("Instance.Export.PackPathInvalid.WithDetail", configPackPath, ex.ToString()),
Lang.Text("Instance.Export.PackPathInvalid.Title"), Lang.Text("Common.Action.Confirm"),
Lang.Text("Common.Action.Cancel")) == 2)
return;
}
if (packPath is null)
{
var extensions = new List<string>();
if (CheckAdvancedModrinth.Checked == false)
extensions.Add(Lang.Text("Instance.Export.ZipFilter"));
if (CheckOptionsPcl.Checked == false)
extensions.Add(Lang.Text("Instance.Export.MrpackFilter"));
packPath = SystemDialogs.SelectSaveFile(Lang.Text("Instance.Export.SelectSaveLocation"),
packName + (string.IsNullOrEmpty(TextExportVersion.Text) ? "" : " " + TextExportVersion.Text),
extensions.Join("|"));
ModBase.Log($"[Export] 手动指定的导出路径:{packPath}");
}
if (string.IsNullOrEmpty(packPath))
return;
// 缓存所需参数
var cacheFolder = ModMain.RequestTaskTempFolder();
var overridesFolder = Path.Combine(cacheFolder, "modpack", "overrides");
var mcInstance = PageInstanceLeft.McInstance;
var pathIndie = mcInstance.PathIndie;
var checkHostedAssets = (bool)!CheckAdvancedInclude.Checked;
var modrinthUploadMode = (bool)CheckAdvancedModrinth.Checked;
var includePCL = (bool)CheckOptionsPcl.Checked;
var includePCLCustom = (bool)(includePCL ? CheckOptionsPclCustom.Checked : (bool?)false);
var allRules = StandardizeLines(GetAllRules(), true).ToList();
var allExtraFiles = StandardizeLines(GetExtraFileLines(), false).ToList();
ModBase.Log($"[Export] 准备导出整合包,共有 {allRules.Count} 条规则,{allExtraFiles.Count} 条追加内容行");
// 构造步骤加载器
var loaders = new List<ModLoader.LoaderBase>();
#region PCL
#if !RELEASE
if (includePCL)
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Instance.Export.Task.DownloadPclRelease"),
loader =>
{
UpdateManager.DownloadLatestPCL(loader);
ModBase.CopyFile(Path.Combine(ModBase.pathTemp, "CE-Latest.exe"),
Path.Combine(cacheFolder, "Plain Craft Launcher.exe"));
})
{
ProgressWeight = 0.5d,
block = false
});
#endif
#endregion
#region
loaders.Add(new ModLoader.LoaderTask<int, List<ModLocalComp.LocalCompFile>>(
Lang.Text("Instance.Export.Task.CopyContent"), loader =>
{
loader.output = [];
// 复制实例文件
var progress = 0;
Action<DirectoryInfo> searchFolder = null;
searchFolder = folder =>
{
// 文件夹:进一步搜索
foreach (var SubFolder in folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly))
{
// 跳过部分又没用文件又多的文件夹,加快搜索
if ((folder.FullName ?? "") == (pathIndie ?? "") &&
new[] { "assets", "versions", "libraries" }.Contains(SubFolder.Name))
continue;
if (new[] { "structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache" }.Contains(
SubFolder.Name))
continue;
searchFolder(SubFolder);
}
// 文件:检查规则并复制
foreach (var Entry in folder.EnumerateFiles("*", SearchOption.TopDirectoryOnly))
{
var relativePath = Entry.FullName.AfterFirst(pathIndie);
// 检查规则
var shouldKeep = false;
foreach (var Rule in allRules)
{
var revert = Rule.StartsWith("!");
if (LikeString(relativePath, Rule.TrimStart('!')))
shouldKeep = !revert;
}
if (!shouldKeep)
continue;
var targetPath = Path.Combine(overridesFolder, relativePath);
ModBase.CopyFile(Entry.FullName, targetPath);
// 若为压缩包,考虑联网获取路径
if (checkHostedAssets &&
new[] { ".zip", ".rar", ".jar", ".disabled", ".old" }.Contains(Entry.Extension.ToLower()) &&
new[] { "mods", "packs", "openloader", "resource" }.Any(s => relativePath.Contains(s)))
{
var modFile = new ModLocalComp.LocalCompFile(targetPath);
var unused = modFile.ModrinthHash; // 提前计算 Hash
unused = modFile.CurseForgeHash.ToString();
loader.output.Add(modFile);
}
// 更新进度(进度并不准确,主要突出一个我还没似)
progress += 1;
if (progress == 25)
{
loader.Progress += (0.94d - loader.Progress) * 0.012d;
progress = 0;
}
}
};
searchFolder(new DirectoryInfo(pathIndie));
ModBase.Log($"[Export] 复制 overrides 文件完成,有 {loader.output.Count} 个文件需要联网检查");
loader.Progress = 0.95d;
// 复制追加内容到根目录
var baseFolder = includePCL ? cacheFolder : Path.Combine(cacheFolder, "modpack");
foreach (var Line in allExtraFiles)
if (Line.EndsWithF(@"\") || Line.EndsWithF("/"))
{
if (Directory.Exists(Line))
ModBase.CopyDirectory(Line, Path.Combine(baseFolder, ModBase.GetFolderNameFromPath(Line)) + @"\");
else
HintService.Hint(Lang.Text("Instance.Export.Config.FolderNotFound", Line), HintType.Error);
}
else if (File.Exists(Line))
{
ModBase.CopyFile(Line, Path.Combine(baseFolder, ModBase.GetFileNameFromPath(Line)));
}
else
{
HintService.Hint(Lang.Text("Instance.Export.Config.FileNotFound", Line), HintType.Error);
}
loader.Progress = 0.97d;
// 复制 PCL 实例设置
ModBase.CopyDirectory(Path.Combine(mcInstance.PathInstance, "PCL"), Path.Combine(overridesFolder, "PCL"));
#if RELEASE
// 复制 PCL 本体
if (includePCL) ModBase.CopyFile(Basics.ExecutablePath, Path.Combine(cacheFolder, Basics.ExecutableName));
#endif
// 复制 PCL 个性化内容
if (includePCLCustom)
{
if (Directory.Exists(Path.Combine(ModBase.exePath, "PCL", "Pictures")))
ModBase.CopyDirectory(Path.Combine(ModBase.exePath, "PCL", "Pictures"), Path.Combine(cacheFolder, "PCL", "Pictures"));
if (Directory.Exists(Path.Combine(ModBase.exePath, "PCL", "Musics")))
ModBase.CopyDirectory(Path.Combine(ModBase.exePath, "PCL", "Musics"), Path.Combine(cacheFolder, "PCL", "Musics"));
if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml")))
ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Custom.xaml"), Path.Combine(cacheFolder, "PCL", "Custom.xaml"));
if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Setup.ini")))
ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Setup.ini"), Path.Combine(cacheFolder, "PCL", "Setup.ini"));
if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "hints.txt")))
ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "hints.txt"), Path.Combine(cacheFolder, "PCL", "hints.txt"));
if (File.Exists(Path.Combine(ModBase.exePath, "PCL", "Logo.png")))
ModBase.CopyFile(Path.Combine(ModBase.exePath, "PCL", "Logo.png"), Path.Combine(cacheFolder, "PCL", "Logo.png"));
}
})
{
ProgressWeight = 5d
});
#endregion
#region
loaders.Add(
new ModLoader.LoaderTask<List<ModLocalComp.LocalCompFile>,
Dictionary<ModLocalComp.LocalCompFile, List<string>>>(Lang.Text("Instance.Export.Task.FetchFileInfo"),
loader =>
{
loader.output = new Dictionary<ModLocalComp.LocalCompFile, List<string>>();
if (!checkHostedAssets)
{
ModBase.Log("[Export] 要求跳过联网获取步骤");
return;
}
if (!loader.input.Any())
{
ModBase.Log("[Export] 没有需要联网检查的文件,跳过联网获取步骤");
return;
}
// 分平台获取下载地址
var endedThreadCount = 0;
var failedExceptions = new List<Exception>();
// 从 Modrinth 获取信息
// 查找对应的文件
// 写入下载地址
ModBase.RunInNewThread(() =>
{
try
{
var modrinthHashes = loader.input.Select(m => m.ModrinthHash);
var modrinthRaw = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest(
"https://api.modrinth.com/v2/version_files", "POST",
$"{{\"hashes\": [\"{modrinthHashes.Join("\",\"")}\"], \"algorithm\": \"sha1\"}}",
"application/json"));
foreach (var ModFile in loader.input)
{
if (!modrinthRaw.ContainsKey(ModFile.ModrinthHash)) continue;
if ((string)modrinthRaw[ModFile.ModrinthHash]?["files"]?[0]["hashes"]?["sha1"] !=
ModFile.ModrinthHash) continue;
loader.output.AddToList(ModFile,
(string)modrinthRaw[ModFile.ModrinthHash]["files"][0]["url"]);
}
ModBase.Log($"[Export] 从 Modrinth 获取到 {modrinthRaw.Count} 个本地资源项的对应信息");
}
catch (Exception ex)
{
ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败");
failedExceptions.Add(ex);
}
finally
{
endedThreadCount += 1;
loader.Progress += 0.45d;
}
}, "Modrinth - " + loaderName);
// 从 CurseForge 获取信息
// 查找对应的文件
// 写入下载地址
ModBase.RunInNewThread(() =>
{
try
{
if (modrinthUploadMode) return;
var curseForgeHashes = loader.input.Select(m => m.CurseForgeHash);
var curseForgeRaw = (JsonNode)((JsonObject)ModBase.GetJson(
ModDownload.DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", "POST",
$"{{\"fingerprints\": [{curseForgeHashes.Join(",")}]}}",
"application/json")))["data"][
"exactMatches"];
foreach (JsonObject ResultJson in curseForgeRaw.AsArray())
{
if (!ResultJson.ContainsKey("file")) continue;
var file = (JsonObject)ResultJson["file"];
if (string.IsNullOrEmpty((string)file["downloadUrl"])) continue;
var modFile = loader.input.FirstOrDefault(m =>
m.CurseForgeHash == file["fileFingerprint"].ToObject<uint>());
if (modFile is null) continue;
loader.output.AddToList(modFile,
ModComp.CompFile.HandleCurseForgeDownloadUrls(file["downloadUrl"].ToString()));
}
ModBase.Log($"[Export] 从 CurseForge 获取到 {curseForgeRaw.AsArray().Count} 个本地资源项的对应信息");
}
catch (Exception ex)
{
ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败");
failedExceptions.Add(ex);
}
finally
{
endedThreadCount += 1;
loader.Progress += 0.45d;
}
}, "CurseForge - " + loaderName); // Modrinth 上传模式下,不能从 CurseForge 获取信息
// 等待线程结束
while (endedThreadCount != 2)
{
if (loader.IsAborted)
return;
Thread.Sleep(10);
}
// 若失败,确认是否继续
if (failedExceptions.Count == 1)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Export.NetCheckPartialFailed.Message"),
Lang.Text("Instance.Export.NetCheckPartialFailed.Title"),
Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel")) == 2)
throw failedExceptions.First();
}
else if (failedExceptions.Count > 1)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Export.NetCheckAllFailed.Message"),
Lang.Text("Instance.Export.NetCheckAllFailed.Title"),
Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel")) == 2)
throw failedExceptions.First();
}
})
{
show = checkHostedAssets,
ProgressWeight = checkHostedAssets ? 2d : 0.01d
});
#endregion
#region
loaders.Add(new ModLoader.LoaderTask<Dictionary<ModLocalComp.LocalCompFile, List<string>>, int>(
Lang.Text("Instance.Export.Task.CreateArchive"),
loader =>
{
// 整理文件列表
var files = new JsonArray();
foreach (var Pair in loader.input)
{
var modFile = Pair.Key;
files.Add(new JsonObject
{
{ "path", Path.GetRelativePath(overridesFolder, modFile.path).Replace(@"\", "/") },
{
"hashes",
new JsonObject
{
{ "sha1", modFile.ModrinthHash }, { "sha512", ModBase.GetFileSHA512(modFile.path) }
}
},
{ "downloads", new JsonArray(Pair.Value.OrderByDescending(u => u.Contains("modrinth.com")).Select(s => (JsonNode)s).ToArray()) },
{ "fileSize", new FileInfo(modFile.path).Length }
});
File.Delete(modFile.path);
}
loader.Progress = 0.2d;
// 导出最终 JSON 文件
var dependencies = new JsonObject { { "minecraft", mcInstance.Info.VanillaName } };
if (mcInstance.Info.HasForge)
dependencies.Add("forge", mcInstance.Info.Forge);
if (mcInstance.Info.HasFabric)
dependencies.Add("fabric-loader", mcInstance.Info.Fabric);
if (mcInstance.Info.HasNeoForge)
dependencies.Add("neoforge", mcInstance.Info.NeoForge);
var resultJson = new JsonObject
{
{ "game", "minecraft" }, { "formatVersion", 1 }, { "versionId", packVersion }, { "name", packName },
{ "summary", mcInstance.Desc }, { "files", files }, { "dependencies", dependencies }
};
File.WriteAllText(Path.Combine(cacheFolder, "modpack", "modrinth.index.json"),
resultJson.ToJsonString(new JsonSerializerOptions(JsonCompat.SerializerOptions) { WriteIndented = true }));
// 打包
Directory.CreateDirectory(ModBase.GetPathFromFullPath(packPath));
if (File.Exists(packPath))
File.Delete(packPath);
if (includePCL)
{
// 首次压缩整合包
ZipFile.CreateFromDirectory(Path.Combine(cacheFolder, "modpack"), Path.Combine(cacheFolder, "modpack.mrpack"));
loader.Progress = 0.5d;
Directory.Delete(Path.Combine(cacheFolder, "modpack"), true);
loader.Progress = 0.6d;
// 二次压缩整合包
ZipFile.CreateFromDirectory(cacheFolder, packPath);
loader.Progress = 0.9d;
}
else
{
// 直接压缩整合包
ZipFile.CreateFromDirectory(Path.Combine(cacheFolder, "modpack"), packPath);
loader.Progress = 0.8d;
}
Directory.Delete(cacheFolder, true);
ModBase.OpenExplorer(packPath);
})
{
ProgressWeight = 6d
});
#endregion
// 启动
var mainLoader = new ModLoader.LoaderCombo<string>(loaderName, loaders)
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
mainLoader.Start();
ModLoader.LoaderTaskbarAdd(mainLoader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
ModMain.frmMain.PageChange(FormMain.PageType.TaskManager);
}
#endregion
private static bool LikeString(string input, string pattern)
{
pattern = pattern.Replace("#", "[0-9]");
var options = new GlobOptions { Evaluation = { CaseInsensitive = true } };
var glob = Glob.Parse(pattern, options);
return glob.IsMatch(input);
}
}
@@ -0,0 +1,422 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceInstall"
PanScroll="{Binding ElementName=PanBack}">
<Grid>
<Grid x:Name="PanAllBack">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
x:Name="PanBack">
<Grid Margin="25,10,25,75">
<StackPanel Grid.Row="1" Name="PanMinecraft" Grid.IsSharedSizeScope="True" Margin="0,0,0,-10"
Visibility="Collapsed">
<StackPanel.RenderTransform>
<TranslateTransform />
</StackPanel.RenderTransform>
</StackPanel>
<StackPanel Grid.Row="1" x:Name="PanSelect" Visibility="Visible" Opacity="0"
IsHitTestVisible="False">
<StackPanel.RenderTransform>
<TranslateTransform />
</StackPanel.RenderTransform>
<local:MyHint Text="{DynamicResource Download.Install.Warning.LegacyOptiFabric}"
Margin="0,10,0,0" x:Name="HintLegacyOptiFabric" Theme="Yellow" />
<local:MyHint Text="{DynamicResource Download.Install.Warning.LegacyFabricApi}"
Margin="0,10,0,0"
x:Name="HintLegacyFabricAPI" Theme="Red" />
<local:MyHint Text="{DynamicResource Download.Install.Warning.FabricApi}"
Margin="0,1,0,7"
x:Name="HintFabricAPI" IsWarn="True" />
<local:MyHint Text="{DynamicResource Download.Install.Warning.OptiFabric}"
Margin="0,1,0,7"
x:Name="HintOptiFabric" IsWarn="True" />
<local:MyHint Text="{DynamicResource Download.Install.Warning.OptiFabricOld}"
Margin="0,1,0,7" x:Name="HintOptiFabricOld" IsWarn="False" />
<local:MyHint Text="{DynamicResource Download.Install.Warning.ModOptiFine}" Margin="0,1,0,7"
x:Name="HintModOptiFine"
IsWarn="False" />
<local:MyCard Margin="0,15,0,0" UseAnimation="False">
<StackPanel Margin="15,7,15,7">
<local:MyListItem x:Name="ItemSelect" IsHitTestVisible="False" Margin="-7,0,0,0"
Height="42" SnapsToDevicePixels="True" Type="None" />
</StackPanel>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.Minecraft}" Height="40" Margin="0,15,0,0"
x:Name="CardMinecraft"
PreviewSwap="CardMinecraft_PreviewSwap">
<Grid x:Name="PanMinecraftInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgMinecraft" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear" />
<TextBlock x:Name="LabMinecraft" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
<Path
Data="M7.736 1.56a1.914 1.914 0 0 1 2.707 2.708l-.234.234l-2.707-2.707zm-.941.942L1.65 7.646a.5.5 0 0 0-.136.255l-.504 2.5a.5.5 0 0 0 .588.59l2.504-.5a.5.5 0 0 0 .255-.137l5.145-5.145z"
Height="12" Width="12" Stretch="Uniform" VerticalAlignment="Center"
Grid.Column="2" Fill="{StaticResource ColorBrushGray4}" Margin="0,1,6,0"
RenderTransformOrigin="0.5,0.5" />
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Instance.Install.Action.ModifyLabel}" Grid.Column="3"
Foreground="{StaticResource ColorBrushGray4}" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.Forge}" Height="40" Margin="0,15,0,0"
x:Name="CardForge" IsSwapped="True"
CanSwap="True" SwapLogoRight="True" PreviewSwap="CardForge_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanForge" />
<Grid x:Name="PanForgeInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Anvil.png" />
<TextBlock x:Name="LabForge" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnForgeClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="Forge_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnForgeClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.Cleanroom}" Height="40"
Margin="0,15,0,0" x:Name="CardCleanroom"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardCleanroom_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanCleanroom" />
<Grid x:Name="PanCleanroomInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgCleanroom" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Cleanroom.png" />
<TextBlock x:Name="LabCleanroom" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnCleanroomClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="Cleanroom_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnCleanroomClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.NeoForge}" Height="40"
Margin="0,15,0,0" x:Name="CardNeoForge"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardNeoForge_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanNeoForge" />
<Grid x:Name="PanNeoForgeInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgNeoForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/NeoForge.png" />
<TextBlock x:Name="LabNeoForge" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnNeoForgeClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="NeoForge_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnNeoForgeClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.Fabric}" Height="40" Margin="0,15,0,0"
x:Name="CardFabric" IsSwapped="True"
CanSwap="True" SwapLogoRight="True" PreviewSwap="CardFabric_PreviewSwap">
<StackPanel Margin="20,40,18,0" VerticalAlignment="Top" Name="PanFabric" />
<Grid x:Name="PanFabricInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Fabric.png" />
<TextBlock x:Name="LabFabric" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnFabricClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="Fabric_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnFabricClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.LegacyFabric}" Height="40"
Margin="0,12,0,0" x:Name="CardLegacyFabric"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardLegacyFabric_PreviewSwap">
<StackPanel Margin="20,40,18,0" VerticalAlignment="Top" Name="PanLegacyFabric" />
<Grid x:Name="PanLegacyFabricInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgLegacyFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Fabric.png" />
<TextBlock x:Name="LabLegacyFabric" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnLegacyFabricClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="LegacyFabric_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnLegacyFabricClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.FabricApi}" Height="40"
Margin="0,15,0,0" x:Name="CardFabricApi"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardFabricApi_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabricApi" />
<Grid x:Name="PanFabricApiInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgFabricApi" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Fabric.png" />
<TextBlock x:Name="LabFabricApi" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnFabricApiClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="FabricApi_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnFabricApiClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.LegacyFabricApi}" Height="40"
Margin="0,12,0,0"
x:Name="CardLegacyFabricApi" IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardLegacyFabricApi_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanLegacyFabricApi" />
<Grid x:Name="PanLegacyFabricApiInfo" Height="18" Margin="132,11,15,0"
VerticalAlignment="Top" Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgLegacyFabricApi" Margin="0,0,7,0" SnapsToDevicePixels="True"
Height="18" RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Fabric.png" />
<TextBlock x:Name="LabLegacyFabricApi" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnLegacyFabricApiClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="LegacyFabricApi_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnLegacyFabricApiClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.LabyMod}" Height="40" Margin="0,15,0,0"
x:Name="CardLabyMod"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardLabyMod_PreviewSwap">
<StackPanel Margin="20,40,18,0" VerticalAlignment="Top" Name="PanLabyMod" />
<Grid x:Name="PanLabyModInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgLabyMod" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/LabyMod.png" />
<TextBlock x:Name="LabLabyMod" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnLabyModClear" Height="30" Width="30" HorizontalAlignment="Right"
MouseLeftButtonUp="LabyMod_Clear"
VerticalAlignment="Top" Margin="0,5,32,0"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnLabyModClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.OptiFine}" Height="40"
Margin="0,15,0,0" x:Name="CardOptiFine"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardOptiFine_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top">
<StackPanel Name="PanOptiFine" />
</StackPanel>
<Grid x:Name="PanOptiFineInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgOptiFine" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/GrassPath.png" />
<TextBlock x:Name="LabOptiFine" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnOptiFineClear" Height="30" Width="30" HorizontalAlignment="Right"
MouseLeftButtonUp="OptiFine_Clear"
VerticalAlignment="Top" Margin="0,5,32,0"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnOptiFineClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.OptiFabric}" Height="40"
Margin="0,15,0,0" x:Name="CardOptiFabric"
IsSwapped="True" CanSwap="True" SwapLogoRight="True"
PreviewSwap="CardOptiFabric_PreviewSwap">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanOptiFabric" />
<Grid x:Name="PanOptiFabricInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgOptiFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18"
RenderOptions.BitmapScalingMode="HighQuality"
Source="pack://application:,,,/images/Blocks/OptiFabric.png" />
<TextBlock x:Name="LabOptiFabric" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnOptiFabricClear" Height="30" Width="30" HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="0,5,32,0" MouseLeftButtonUp="OptiFabric_Clear"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnOptiFabricClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
<local:MyCard Title="{DynamicResource Common.Installation.LiteLoader}" Height="40"
Margin="0,15,0,0" x:Name="CardLiteLoader"
PreviewSwap="CardLiteLoader_PreviewSwap"
IsSwapped="True" CanSwap="True" SwapLogoRight="True">
<StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanLiteLoader" />
<Grid x:Name="PanLiteLoaderInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top"
Tag="True">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Image x:Name="ImgLiteLoader" Margin="-1,0,6,0" SnapsToDevicePixels="True" Height="20"
RenderOptions.BitmapScalingMode="Linear"
Source="pack://application:,,,/images/Blocks/Egg.png" />
<TextBlock x:Name="LabLiteLoader" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Grid.Column="1" />
</Grid>
<Grid x:Name="BtnLiteLoaderClear" Height="30" Width="30" HorizontalAlignment="Right"
MouseLeftButtonUp="LiteLoader_Clear"
VerticalAlignment="Top" Margin="0,5,32,0"
Background="{StaticResource ColorBrushSemiTransparent}">
<Path x:Name="BtnLiteLoaderClearInner" Height="10" Width="10" Stretch="Uniform"
Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
</Grid>
</local:MyCard>
</StackPanel>
</Grid>
</local:MyScrollViewer>
<local:MyExtraTextButton x:Name="BtnSelectStart"
Text="{DynamicResource Instance.Install.Action.StartModify}"
Click="BtnSelectStart_Click"
HorizontalAlignment="Center" VerticalAlignment="Bottom" IsEnabled="False"
LogoScale="0.95"
SvgIcon="lucide/download" />
</Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True"
x:Name="PanLoad" UseAnimation="False">
<local:MyLoading Text="" Margin="20,20,20,17" x:Name="LoadMinecraft" HorizontalAlignment="Center"
VerticalAlignment="Center" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadOptiFine" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadForge" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadNeoForge" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadCleanroom" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadLiteLoader" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadFabric" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadLegacyFabric" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadFabricApi" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadLegacyFabricApi" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadLabyMod" HasAnimation="False" />
<local:MyLoading Visibility="Collapsed" x:Name="LoadOptiFabric" HasAnimation="False" />
</local:MyCard>
</Grid>
</local:MyPageRight>
@@ -0,0 +1,2553 @@
using System.Collections;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.UI;
using PCL.Core.Utils;
namespace PCL;
public partial class PageInstanceInstall
{
private enum InstallAction
{
Modify,
Reset
}
private bool isLoad;
private string lastVersionName;
private InstallAction _installAction;
public PageInstanceInstall()
{
Initialized += (a, b) => LoaderInit();
Loaded += (a, b) => Init();
InitializeComponent();
LoadMinecraft.Text = Lang.Text("Download.Version.LoadingList");
}
private void LoaderInit()
{
disabledPageAnimControls.Add(BtnSelectStart);
// PageLoaderInit(LoadMinecraft, PanLoad, PanBack, Nothing, DlClientListLoader, AddressOf LoadMinecraft_OnFinish)
PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, null, ModDownload.dlClientListLoader, _ => GetCurrentInfo());
LoadOptiFine.StateChanged += (_, _, _) => { OptiFine_Loaded(); ReloadSelected(); };
LoadLiteLoader.StateChanged += (_, _, _) => { LiteLoader_Loaded(); ReloadSelected(); };
LoadForge.StateChanged += (_, _, _) => { Forge_Loaded(); ReloadSelected(); };
LoadNeoForge.StateChanged += (_, _, _) => { NeoForge_Loaded(); ReloadSelected(); };
LoadCleanroom.StateChanged += (_, _, _) => { Cleanroom_Loaded(); ReloadSelected(); };
LoadFabric.StateChanged += (_, _, _) => { Fabric_Loaded(); ReloadSelected(); };
LoadFabricApi.StateChanged += (_, _, _) => { FabricApi_Loaded(); ReloadSelected(); };
LoadLegacyFabric.StateChanged += (_, _, _) => { LegacyFabric_Loaded(); ReloadSelected(); };
LoadLegacyFabricApi.StateChanged += (_, _, _) => { LegacyFabricApi_Loaded(); ReloadSelected(); };
LoadOptiFabric.StateChanged += (_, _, _) => { OptiFabric_Loaded(); ReloadSelected(); };
LoadLabyMod.StateChanged += (_, _, _) => { LabyMod_Loaded(); ReloadSelected(); };
PageExit += () => isInSelectPage = false;
}
private void Init()
{
PanBack.ScrollToHome();
GetCurrentInfo();
var needRefresh = lastVersionName is null || (lastVersionName ?? "") != (_vanillaName ?? "");
lastVersionName = _vanillaName;
ModDownload.dlOptiFineListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlLiteLoaderListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlFabricListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlNeoForgeListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlCleanroomListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlLabyModListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlLegacyFabricListLoader.Start(isForceRestart: needRefresh);
ModDownload.dlFabricApiLoader.Start(isForceRestart: needRefresh);
ModDownload.dlLegacyFabricApiLoader.Start(isForceRestart: needRefresh);
ModDownload.dlOptiFabricLoader.Start(isForceRestart: needRefresh);
// 非重复加载部分
if (isLoad)
{
ReloadSelected();
return;
}
isLoad = true;
ModDownloadLib.McDownloadForgeRecommendedRefresh();
LoadOptiFine.State = ModDownload.dlOptiFineListLoader;
LoadLiteLoader.State = ModDownload.dlLiteLoaderListLoader;
LoadFabric.State = ModDownload.dlFabricListLoader;
LoadFabricApi.State = ModDownload.dlFabricApiLoader;
LoadNeoForge.State = ModDownload.dlNeoForgeListLoader;
LoadCleanroom.State = ModDownload.dlCleanroomListLoader;
LoadOptiFabric.State = ModDownload.dlOptiFabricLoader;
LoadLabyMod.State = ModDownload.dlLabyModListLoader;
LoadLegacyFabric.State = ModDownload.dlLegacyFabricListLoader;
LoadLegacyFabricApi.State = ModDownload.dlLegacyFabricApiLoader;
}
#region
private void BtnSelectStart_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
// Quilt 实例无法通过安装管线重装/修改(已移除 Quilt 安装支持)
if (PageInstanceLeft.McInstance.Info.HasQuilt)
{
HintService.Hint(Lang.Text("Instance.Overall.Reset.QuiltUnsupported"));
return;
}
// 确认版本隔离
if (selectedLoaderName is not null &&
(Config.Launch.IndieSolutionV2 == 0 ||
Config.Launch.IndieSolutionV2 == 2))
if (ModMain.MyMsgBox(
Lang.Text("Download.Install.InstanceIsolation.Warning.Message"), Lang.Text("Download.Install.InstanceIsolation.Warning.Title"), Lang.Text("Download.Install.InstanceIsolation.Warning.Cancel"), Lang.Text("Download.Install.InstanceIsolation.Warning.Continue")) == 1)
return;
if (_installAction == InstallAction.Reset)
if (ModMain.MyMsgBox(
Lang.Text("Instance.Install.Reset.Message"),
Lang.Text("Instance.Install.Reset.Title"),
Lang.Text("Common.Action.Continue"),
Lang.Text("Common.Action.Cancel")
) == 2)
return;
// 删除 LabyMod Neo 文件
if ((PageInstanceLeft.McInstance.PathIndie ?? "") != (PageInstanceLeft.McInstance.PathInstance ?? "") &&
PageInstanceLeft.McInstance.Info.HasLabyMod)
Directory.Delete(System.IO.Path.Combine(PageInstanceLeft.McInstance.PathIndie, "labymod-neo"), true);
// 备份实例核心文件
ModBase.CopyFile(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name + ".json",
PageInstanceLeft.McInstance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.McInstance.Name + ".json");
if (File.Exists(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name + ".jar"))
ModBase.CopyFile(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name + ".jar",
PageInstanceLeft.McInstance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.McInstance.Name +
".jar");
// 确认独立 API (如 Fabric API 等) 是否需要被修改
if (selectedFabricApi?.Equals(_currentFabricApi) == true)
selectedFabricApi = null;
if (selectedLegacyFabricApi?.Equals(_currentLegacyFabricApi) == true)
selectedLegacyFabricApi = null;
if (selectedOptiFabric?.Equals(_currentOptiFabric) == true)
selectedOptiFabric = null;
// 提交安装申请
var request = new ModDownloadLib.McInstallRequest
{
targetInstanceName = PageInstanceLeft.McInstance.Name,
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{PageInstanceLeft.McInstance.Name}\",
minecraftJson = _vanillaData?["url"].ToString(),
minecraftName = _vanillaName,
optiFineEntry = selectedOptiFine,
forgeEntry = selectedForge,
neoForgeEntry = selectedNeoForge,
neoForgeVersion = selectedNeoForgeVersion,
cleanroomEntry = selectedCleanroom,
cleanroomVersion = selectedCleanroomVersion,
fabricVersion = selectedFabric,
fabricApi = selectedFabricApi,
optiFabric = selectedOptiFabric,
liteLoaderEntry = selectedLiteLoader,
labyModChannel = selectedLabyModChannel,
labyModCommitRef = selectedLabyModCommitRef,
legacyFabricVersion = selectedLegacyFabric,
legacyFabricApi = selectedLegacyFabricApi
};
BtnSelectStart.IsEnabled = false;
if (!ModDownloadLib.McInstall(request, _installAction == InstallAction.Modify ? Lang.Text("Instance.Install.Action.ModifyLabel") : Lang.Text("Common.Action.Reset")))
return;
// 删除旧的独立 API 文件
if (selectedFabricApi is not null && _currentFabricApiPath is not null)
File.Delete(_currentFabricApiPath);
if (selectedLegacyFabricApi is not null && _currentLegacyFabricApiPath is not null)
File.Delete(_currentLegacyFabricApiPath);
if (selectedOptiFabric is not null && _currentOptiFabricPath is not null)
File.Delete(_currentOptiFabricPath);
// 返回主页
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
}
#endregion
private string GetLoaderError(MyLoading loader)
{
if (loader is null || !loader.State.IsLoader)
return Lang.Text("Download.Install.State.Getting");
switch (loader.State.LoadingState)
{
case MyLoading.MyLoadingState.Run:
{
return Lang.Text("Download.Install.State.Getting");
}
case MyLoading.MyLoadingState.Error:
{
var message = ((ModLoader.LoaderBase)loader.State).Error.Message;
return message == Lang.Text("Download.Install.State.NoVersion") ? Lang.Text("Download.Install.State.NoVersion") : Lang.Text("Download.Install.State.GetFailed", message);
}
case MyLoading.MyLoadingState.Unloaded:
{
return Lang.Text("Download.Install.State.UnknownUnloaded");
}
default:
{
return null;
}
}
}
#region
// 页面切换动画
public bool isInSelectPage;
private bool isFirstLoaded;
private void EnterSelectPage()
{
if (isInSelectPage)
return;
isInSelectPage = true;
disabledPageAnimControls.Remove(BtnSelectStart);
BtnSelectStart.Show = true;
autoSelectedFabricApi = false;
autoSelectedOptiFabric = false;
PanSelect.Visibility = Visibility.Visible;
PanSelect.IsHitTestVisible = true;
PanMinecraft.IsHitTestVisible = false;
PanBack.IsHitTestVisible = false;
PanBack.ScrollToHome();
CardMinecraft.IsSwapped = true;
CardOptiFine.IsSwapped = true;
CardLiteLoader.IsSwapped = true;
CardForge.IsSwapped = true;
CardNeoForge.IsSwapped = true;
CardCleanroom.IsSwapped = true;
CardFabric.IsSwapped = true;
CardFabricApi.IsSwapped = true;
CardOptiFabric.IsSwapped = true;
CardLabyMod.IsSwapped = true;
CardLegacyFabric.IsSwapped = true;
CardLegacyFabricApi.IsSwapped = true;
if (!(bool)States.Hint.InstallPageBack)
{
States.Hint.InstallPageBack = true;
HintService.Hint(Lang.Text("Download.Install.Hint.MinecraftBack"));
}
// 如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复
foreach (var Card in GetAllAnimControls(PanSelect))
{
Card.Opacity = 1d;
Card.RenderTransform = new TranslateTransform();
}
// 启动 Forge 加载
if (McInstanceInfo.IsFormatFit(_vanillaName))
{
var forgeLoader =
new ModLoader.LoaderTask<string, List<ModDownload.DlForgeVersionEntry>>(
"DlForgeVersion " + _vanillaName, ModDownload.DlForgeVersionMain);
LoadForge.State = forgeLoader;
forgeLoader.Start(_vanillaName);
}
// 启动 Fabric API、Legacy Fabric API、OptiFabric 加载
ModDownload.dlFabricApiLoader.Start();
ModDownload.dlLegacyFabricApiLoader.Start();
ModDownload.dlOptiFabricLoader.Start();
ModAnimation.AniStart(new[]
{
ModAnimation.AaOpacity(PanMinecraft, -PanMinecraft.Opacity, 100, 10),
ModAnimation.AaCode(() =>
{
PanBack.ScrollToHome();
OptiFine_Loaded();
LiteLoader_Loaded();
Forge_Loaded();
NeoForge_Loaded();
Cleanroom_Loaded();
Fabric_Loaded();
LegacyFabric_Loaded();
FabricApi_Loaded();
LegacyFabricApi_Loaded();
LabyMod_Loaded();
OptiFabric_Loaded();
ReloadSelected();
}, after: true),
ModAnimation.AaOpacity(PanSelect, 1d - PanSelect.Opacity, 250, 150),
ModAnimation.AaCode(() =>
{
PanMinecraft.Visibility = Visibility.Collapsed;
PanBack.IsHitTestVisible = true;
// 初始化 Binding
if (isFirstLoaded)
return;
isFirstLoaded = true;
BtnOptiFineClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardOptiFine.MainTextBlock, Mode = BindingMode.OneWay });
BtnLiteLoaderClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardLiteLoader.MainTextBlock, Mode = BindingMode.OneWay });
BtnForgeClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardForge.MainTextBlock, Mode = BindingMode.OneWay });
BtnLegacyFabricClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardLegacyFabric.MainTextBlock, Mode = BindingMode.OneWay });
BtnNeoForgeClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardNeoForge.MainTextBlock, Mode = BindingMode.OneWay });
BtnCleanroomClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardCleanroom.MainTextBlock, Mode = BindingMode.OneWay });
BtnFabricClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardFabric.MainTextBlock, Mode = BindingMode.OneWay });
BtnLegacyFabricApiClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground")
{ Source = CardLegacyFabricApi.MainTextBlock, Mode = BindingMode.OneWay });
BtnFabricApiClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardFabricApi.MainTextBlock, Mode = BindingMode.OneWay });
BtnLabyModClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardLabyMod.MainTextBlock, Mode = BindingMode.OneWay });
BtnOptiFabricClearInner.SetBinding(Shape.FillProperty,
new Binding("Foreground") { Source = CardOptiFabric.MainTextBlock, Mode = BindingMode.OneWay });
}, after: true)
}, "FrmInstanceInstall SelectPageSwitch", true);
}
public void ExitSelectPage()
{
if (!isInSelectPage)
return;
isInSelectPage = false;
LoadMinecraft_OnFinish();
disabledPageAnimControls.Add(BtnSelectStart);
BtnSelectStart.Show = false;
ClearSelected(); // 清除已选择项
PanMinecraft.Visibility = Visibility.Visible;
PanSelect.IsHitTestVisible = false;
PanMinecraft.IsHitTestVisible = true;
PanBack.IsHitTestVisible = false;
PanBack.ScrollToHome();
ModAnimation.AniStart(new[]
{
ModAnimation.AaOpacity(PanSelect, -PanSelect.Opacity, 90, 10),
ModAnimation.AaCode(() => PanBack.ScrollToHome(), after: true),
ModAnimation.AaOpacity(PanMinecraft, 1d - PanMinecraft.Opacity, 150, 100),
ModAnimation.AaCode(() =>
{
PanSelect.Visibility = Visibility.Collapsed;
PanBack.IsHitTestVisible = true;
}, after: true)
}, "FrmInstanceInstall SelectPageSwitch");
}
// 页面切换触发
public void MinecraftSelected(MyListItem sender, MouseButtonEventArgs e)
{
_vanillaName = sender.Title;
_vanillaData = (JsonObject)sender.Tag;
_vanillaIcon = sender.Logo;
EnterSelectPage();
}
private void CardMinecraft_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
ExitSelectPage();
e.handled = true;
}
#endregion
#region
// Minecraft
private string? _vanillaName;
private JsonObject? _vanillaData;
private string? _vanillaIcon;
private int VanillaDrop => McInstanceInfo.VersionToDrop(_vanillaName, true);
// OptiFine
private ModDownload.DlOptiFineListEntry? selectedOptiFine;
/// <summary>
/// 选定的 Mod Loader 名称,内容应为 Forge / NeoForge / Fabric / Cleanroom / LabyMod / LegacyFabric
/// </summary>
private string? selectedLoaderName;
/// <summary>
/// 选定的 Mod Loader API 名称,内容应为 Fabric API
/// </summary>
private string? selectedAPIName;
// LiteLoader
private ModDownload.DlLiteLoaderListEntry? selectedLiteLoader;
// Forge
private ModDownload.DlForgeVersionEntry? selectedForge;
// Cleanroom
private ModDownload.DlCleanroomListEntry? selectedCleanroom;
private string? selectedCleanroomVersion;
// NeoForge
private ModDownload.DlNeoForgeListEntry? selectedNeoForge;
private string? selectedNeoForgeVersion;
// Fabric
private string? selectedFabric;
// FabricApi
private ModComp.CompFile? selectedFabricApi;
// LegacyFabric
private string? selectedLegacyFabric;
// Legacy FabricApi
private ModComp.CompFile? selectedLegacyFabricApi;
// LabyMod
private string? selectedLabyModChannel;
private string? selectedLabyModCommitRef;
private string? selectedLabyModVersion;
// OptiFabric
private ModComp.CompFile? selectedOptiFabric;
private bool _ReloadSelected_Ongoing; // #3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload
/// <summary>
/// 重载已选择的项目的显示。
/// </summary>
private void ReloadSelected()
{
if (_vanillaName is null || _ReloadSelected_Ongoing)
return;
_ReloadSelected_Ongoing = true;
try
{
var selectedInfo = GetSelectInfo();
// 主预览
ItemSelect.Title = PageInstanceLeft.McInstance.Name;
ItemSelect.Logo = GetSelectLogo();
BtnSelectStart.IsEnabled = true;
if ((selectedInfo ?? "") == (currentInfo ?? ""))
{
ItemSelect.Info = selectedInfo;
BtnSelectStart.Text = Lang.Text("Instance.Install.Action.StartReset");
_installAction = InstallAction.Reset;
BtnSelectStart.SvgIcon = "lucide/rotate-ccw";
}
else
{
ItemSelect.Info = currentInfo + " → " + selectedInfo;
BtnSelectStart.Text = Lang.Text("Instance.Install.Action.StartModify");
_installAction = InstallAction.Modify;
BtnSelectStart.SvgIcon = "lucide/pencil";
}
// Minecraft
ImgMinecraft.Source = new MyBitmap(_vanillaIcon);
LabMinecraft.Text = _vanillaName;
LabMinecraft.Foreground = ThemeManager.colorGray1;
// OptiFine
if (!McVersionComparer.CompareVersionGe(_vanillaName, "1.7.2"))
{
CardOptiFine.Visibility = Visibility.Collapsed;
}
else
{
CardOptiFine.Visibility = Visibility.Visible;
var optiFineError = LoadOptiFineGetError();
CardOptiFine.MainSwap.Visibility = optiFineError is null ? Visibility.Visible : Visibility.Collapsed;
if (optiFineError is not null)
CardOptiFine.IsSwapped = true;
SetPanelVisibility(PanOptiFineInfo, CardOptiFine.IsSwapped);
if (selectedOptiFine is null)
{
BtnOptiFineClear.Visibility = Visibility.Collapsed;
ImgOptiFine.Visibility = Visibility.Collapsed;
LabOptiFine.Text = optiFineError ?? Lang.Text("Download.Install.State.CanAdd");
LabOptiFine.Foreground = ThemeManager.colorGray4;
}
else
{
BtnOptiFineClear.Visibility = Visibility.Visible;
ImgOptiFine.Visibility = Visibility.Visible;
LabOptiFine.Text = selectedOptiFine.DisplayName.Replace(_vanillaName + " ", "");
LabOptiFine.Foreground = ThemeManager.colorGray1;
}
}
// LiteLoader
if (!McInstanceInfo.IsFormatFit(_vanillaName)
|| !McVersionComparer.CompareVersionGe(_vanillaName, "1.5.2")
|| !McVersionComparer.CompareVersionGe("1.12.2", _vanillaName))
{
CardLiteLoader.Visibility = Visibility.Collapsed;
}
else
{
CardLiteLoader.Visibility = Visibility.Visible;
var liteLoaderError = LoadLiteLoaderGetError();
CardLiteLoader.MainSwap.Visibility = liteLoaderError is null ? Visibility.Visible : Visibility.Collapsed;
if (liteLoaderError is not null)
CardLiteLoader.IsSwapped = true; // 例如在同时展开卡片时选择了不兼容项则强制折叠
SetPanelVisibility(PanLiteLoaderInfo, CardLiteLoader.IsSwapped);
if (selectedLiteLoader is null)
{
BtnLiteLoaderClear.Visibility = Visibility.Collapsed;
ImgLiteLoader.Visibility = Visibility.Collapsed;
LabLiteLoader.Text = liteLoaderError ?? Lang.Text("Download.Install.State.CanAdd");
LabLiteLoader.Foreground = ThemeManager.colorGray4;
}
else
{
BtnLiteLoaderClear.Visibility = Visibility.Visible;
ImgLiteLoader.Visibility = Visibility.Visible;
LabLiteLoader.Text = selectedLiteLoader.Inherit;
LabLiteLoader.Foreground = ThemeManager.colorGray1;
}
}
// Forge
if (!McInstanceInfo.IsFormatFit(_vanillaName)
|| !McVersionComparer.CompareVersionGe(_vanillaName, "1.1"))
{
CardForge.Visibility = Visibility.Collapsed;
}
else
{
CardForge.Visibility = Visibility.Visible;
var forgeError = LoadForgeGetError();
CardForge.MainSwap.Visibility = forgeError is null ? Visibility.Visible : Visibility.Collapsed;
if (forgeError is not null)
CardForge.IsSwapped = true;
SetPanelVisibility(PanForgeInfo, CardForge.IsSwapped);
if (selectedForge is null)
{
BtnForgeClear.Visibility = Visibility.Collapsed;
ImgForge.Visibility = Visibility.Collapsed;
LabForge.Text = forgeError ?? Lang.Text("Download.Install.State.CanAdd");
LabForge.Foreground = ThemeManager.colorGray4;
}
else
{
BtnForgeClear.Visibility = Visibility.Visible;
ImgForge.Visibility = Visibility.Visible;
LabForge.Text = selectedForge.VersionName;
LabForge.Foreground = ThemeManager.colorGray1;
}
}
// Cleanroom
if (_vanillaName == "1.12.2")
{
CardCleanroom.Visibility = Visibility.Visible;
var cleanroomError = LoadCleanroomGetError();
CardCleanroom.MainSwap.Visibility = cleanroomError is null ? Visibility.Visible : Visibility.Collapsed;
if (cleanroomError is not null)
CardCleanroom.IsSwapped = true;
SetPanelVisibility(PanCleanroomInfo, CardCleanroom.IsSwapped);
if (selectedCleanroom is null)
{
BtnCleanroomClear.Visibility = Visibility.Collapsed;
ImgCleanroom.Visibility = Visibility.Collapsed;
LabCleanroom.Text = cleanroomError ?? Lang.Text("Download.Install.State.CanAdd");
LabCleanroom.Foreground = ThemeManager.colorGray4;
}
else
{
BtnCleanroomClear.Visibility = Visibility.Visible;
ImgCleanroom.Visibility = Visibility.Visible;
LabCleanroom.Text = selectedCleanroom.VersionName;
LabCleanroom.Foreground = ThemeManager.colorGray1;
}
}
else
{
CardCleanroom.Visibility = Visibility.Collapsed;
}
// NeoForge
if (!McVersionComparer.CompareVersionGe(_vanillaName, "1.20.1"))
{
CardNeoForge.Visibility = Visibility.Collapsed;
}
else
{
CardNeoForge.Visibility = Visibility.Visible;
var neoForgeError = LoadNeoForgeGetError();
CardNeoForge.MainSwap.Visibility = neoForgeError is null ? Visibility.Visible : Visibility.Collapsed;
if (neoForgeError is not null)
CardNeoForge.IsSwapped = true;
SetPanelVisibility(PanNeoForgeInfo, CardNeoForge.IsSwapped);
if (selectedNeoForge is null)
{
BtnNeoForgeClear.Visibility = Visibility.Collapsed;
ImgNeoForge.Visibility = Visibility.Collapsed;
LabNeoForge.Text = neoForgeError ?? Lang.Text("Download.Install.State.CanAdd");
LabNeoForge.Foreground = ThemeManager.colorGray4;
}
else
{
BtnNeoForgeClear.Visibility = Visibility.Visible;
ImgNeoForge.Visibility = Visibility.Visible;
LabNeoForge.Text = selectedNeoForge.VersionName;
LabNeoForge.Foreground = ThemeManager.colorGray1;
}
}
// Fabric
if (VanillaDrop < 130
|| (VanillaDrop == 130 && !McVersionComparer.CompareVersionGe(_vanillaName, "18w43b")))
{
CardFabric.Visibility = Visibility.Collapsed;
}
else
{
CardFabric.Visibility = Visibility.Visible;
var fabricError = LoadFabricGetError();
CardFabric.MainSwap.Visibility = fabricError is null ? Visibility.Visible : Visibility.Collapsed;
if (fabricError is not null)
CardFabric.IsSwapped = true;
SetPanelVisibility(PanFabricInfo, CardFabric.IsSwapped);
if (selectedFabric is null)
{
BtnFabricClear.Visibility = Visibility.Collapsed;
ImgFabric.Visibility = Visibility.Collapsed;
LabFabric.Text = fabricError ?? Lang.Text("Download.Install.State.CanAdd");
LabFabric.Foreground = ThemeManager.colorGray4;
}
else
{
BtnFabricClear.Visibility = Visibility.Visible;
ImgFabric.Visibility = Visibility.Visible;
LabFabric.Text = selectedFabric.Replace("+build", "");
LabFabric.Foreground = ThemeManager.colorGray1;
}
}
// FabricApi
if (selectedFabric is null)
{
CardFabricApi.Visibility = Visibility.Collapsed;
}
else
{
CardFabricApi.Visibility = Visibility.Visible;
var fabricApiError = LoadFabricApiGetError();
CardFabricApi.MainSwap.Visibility = fabricApiError is null ? Visibility.Visible : Visibility.Collapsed;
if (fabricApiError is not null || selectedFabric is null)
CardFabricApi.IsSwapped = true;
SetPanelVisibility(PanFabricApiInfo, CardFabricApi.IsSwapped);
if (selectedFabricApi is null)
{
BtnFabricApiClear.Visibility = Visibility.Collapsed;
ImgFabricApi.Visibility = Visibility.Collapsed;
LabFabricApi.Text = fabricApiError ?? Lang.Text("Download.Install.State.CanAdd");
LabFabricApi.Foreground = ThemeManager.colorGray4;
}
else
{
BtnFabricApiClear.Visibility = Visibility.Visible;
ImgFabricApi.Visibility = Visibility.Visible;
LabFabricApi.Text = selectedFabricApi.DisplayName.Split("]")[1].Replace("Fabric API ", "")
.Replace(" build ", ".").Split("+").First().Trim();
LabFabricApi.Foreground = ThemeManager.colorGray1;
}
}
// LegacyFabric
if (VanillaDrop < 30 || VanillaDrop > 130)
{
CardLegacyFabric.Visibility = Visibility.Collapsed;
}
else
{
CardLegacyFabric.Visibility = Visibility.Visible;
var legacyFabricError = LoadLegacyFabricGetError();
CardLegacyFabric.MainSwap.Visibility =
legacyFabricError is null ? Visibility.Visible : Visibility.Collapsed;
if (legacyFabricError is not null)
CardLegacyFabric.IsSwapped = true;
SetPanelVisibility(PanLegacyFabricInfo, CardLegacyFabric.IsSwapped);
if (selectedLegacyFabric is null)
{
BtnLegacyFabricClear.Visibility = Visibility.Collapsed;
ImgLegacyFabric.Visibility = Visibility.Collapsed;
LabLegacyFabric.Text = legacyFabricError ?? Lang.Text("Download.Install.State.CanAdd");
LabLegacyFabric.Foreground = ThemeManager.colorGray4;
}
else
{
BtnLegacyFabricClear.Visibility = Visibility.Visible;
ImgLegacyFabric.Visibility = Visibility.Visible;
LabLegacyFabric.Text = selectedLegacyFabric.Replace("+build", "");
LabLegacyFabric.Foreground = ThemeManager.colorGray1;
}
}
// LegacyFabricApi
if (selectedLegacyFabric is null)
{
CardLegacyFabricApi.Visibility = Visibility.Collapsed;
}
else
{
CardLegacyFabricApi.Visibility = Visibility.Visible;
var legacyFabricApiError = LoadLegacyFabricApiGetError();
CardLegacyFabricApi.MainSwap.Visibility =
legacyFabricApiError is null ? Visibility.Visible : Visibility.Collapsed;
if (legacyFabricApiError is not null || selectedLegacyFabric is null)
CardLegacyFabricApi.IsSwapped = true;
SetPanelVisibility(PanLegacyFabricApiInfo, CardLegacyFabricApi.IsSwapped);
if (selectedLegacyFabricApi is null)
{
BtnLegacyFabricApiClear.Visibility = Visibility.Collapsed;
ImgLegacyFabricApi.Visibility = Visibility.Collapsed;
LabLegacyFabricApi.Text = legacyFabricApiError ?? Lang.Text("Download.Install.State.CanAdd");
LabLegacyFabricApi.Foreground = ThemeManager.colorGray4;
}
else
{
BtnLegacyFabricApiClear.Visibility = Visibility.Visible;
ImgLegacyFabricApi.Visibility = Visibility.Visible;
LabLegacyFabricApi.Text = selectedLegacyFabricApi.DisplayName.Replace("Legacy Fabric API ", "");
LabLegacyFabricApi.Foreground = ThemeManager.colorGray1;
}
}
// LabyMod
if (!McInstanceInfo.IsFormatFit(_vanillaName)
|| !McVersionComparer.CompareVersionGe(_vanillaName, "1.8.9"))
{
CardLabyMod.Visibility = Visibility.Collapsed;
}
else
{
CardLabyMod.Visibility = Visibility.Visible;
var labyModError = LoadLabyModGetError();
CardLabyMod.MainSwap.Visibility = labyModError is null ? Visibility.Visible : Visibility.Collapsed;
if (labyModError is not null)
CardLabyMod.IsSwapped = true;
SetPanelVisibility(PanLabyModInfo, CardLabyMod.IsSwapped);
if (selectedLabyModVersion is null)
{
BtnLabyModClear.Visibility = Visibility.Collapsed;
ImgLabyMod.Visibility = Visibility.Collapsed;
LabLabyMod.Text = labyModError ?? Lang.Text("Download.Install.State.CanAdd");
LabLabyMod.Foreground = ThemeManager.colorGray4;
}
else
{
BtnLabyModClear.Visibility = Visibility.Visible;
ImgLabyMod.Visibility = Visibility.Visible;
LabLabyMod.Text = selectedLabyModVersion;
LabLabyMod.Foreground = ThemeManager.colorGray1;
}
}
// OptiFabric
if (selectedFabric is null || selectedOptiFine is null)
{
CardOptiFabric.Visibility = Visibility.Collapsed;
}
else
{
CardOptiFabric.Visibility = Visibility.Visible;
var optiFabricError = LoadOptiFabricGetError();
CardOptiFabric.MainSwap.Visibility = optiFabricError is null ? Visibility.Visible : Visibility.Collapsed;
if (optiFabricError is not null || selectedFabric is null)
CardOptiFabric.IsSwapped = true;
SetPanelVisibility(PanOptiFabricInfo, CardOptiFabric.IsSwapped);
if (selectedOptiFabric is null)
{
BtnOptiFabricClear.Visibility = Visibility.Collapsed;
ImgOptiFabric.Visibility = Visibility.Collapsed;
LabOptiFabric.Text = optiFabricError ?? Lang.Text("Download.Install.State.CanAdd");
LabOptiFabric.Foreground = ThemeManager.colorGray4;
}
else
{
BtnOptiFabricClear.Visibility = Visibility.Visible;
ImgOptiFabric.Visibility = Visibility.Visible;
LabOptiFabric.Text = selectedOptiFabric.DisplayName.ToLower().Replace("optifabric-", "")
.Replace(".jar", "").Trim().TrimStart('v');
LabOptiFabric.Foreground = ThemeManager.colorGray1;
}
}
// 主警告
if (selectedFabric is not null && selectedFabricApi is null)
HintFabricAPI.Visibility = Visibility.Visible;
else
HintFabricAPI.Visibility = Visibility.Collapsed;
if (selectedLegacyFabric is not null && selectedLegacyFabricApi is null)
HintLegacyFabricAPI.Visibility = Visibility.Visible;
else
HintLegacyFabricAPI.Visibility = Visibility.Collapsed;
if ((selectedFabric is not null || selectedLegacyFabric is not null) && selectedOptiFine is not null &&
selectedOptiFabric is null)
{
if (VanillaDrop >= 140 && VanillaDrop <= 150)
{
HintOptiFabric.Visibility = Visibility.Collapsed;
HintLegacyOptiFabric.Visibility = Visibility.Collapsed;
HintOptiFabricOld.Visibility = Visibility.Visible;
}
else if (selectedLegacyFabric is not null)
{
HintOptiFabric.Visibility = Visibility.Collapsed;
HintLegacyOptiFabric.Visibility = Visibility.Visible;
HintOptiFabricOld.Visibility = Visibility.Collapsed;
}
else
{
HintOptiFabric.Visibility = Visibility.Visible;
HintOptiFabricOld.Visibility = Visibility.Collapsed;
HintLegacyOptiFabric.Visibility = Visibility.Collapsed;
}
}
else
{
HintOptiFabric.Visibility = Visibility.Collapsed;
HintOptiFabricOld.Visibility = Visibility.Collapsed;
HintLegacyOptiFabric.Visibility = Visibility.Collapsed;
}
if (VanillaDrop >= 160 && selectedOptiFine is not null &&
(selectedForge is not null || selectedFabric is not null))
HintModOptiFine.Visibility = Visibility.Visible;
else
HintModOptiFine.Visibility = Visibility.Collapsed;
// 结束
}
finally
{
_ReloadSelected_Ongoing = false;
}
}
/// <summary>
/// 清空已选择的项目。
/// </summary>
private void ClearSelected()
{
_vanillaName = null;
_vanillaData = null;
_vanillaIcon = null;
selectedOptiFine = null;
selectedLiteLoader = null;
selectedLoaderName = null;
selectedAPIName = null;
selectedForge = null;
selectedNeoForge = null;
selectedNeoForgeVersion = null;
selectedCleanroom = null;
selectedCleanroomVersion = null;
selectedFabric = null;
selectedFabricApi = null;
selectedOptiFabric = null;
selectedLabyModCommitRef = null;
selectedLabyModVersion = null;
selectedLabyModChannel = null;
selectedLegacyFabric = null;
selectedLegacyFabricApi = null;
}
// 信息栏动画
private void SetPanelVisibility(Grid panel, bool visible)
{
if (Equals(panel.Tag, visible.ToString()))
return;
panel.Tag = visible.ToString();
if (visible)
ModAnimation.AniStart(
new[]
{
ModAnimation.AaTranslateY(panel, -((TranslateTransform)panel.RenderTransform).Y, 150,
ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaOpacity(panel, 1d - panel.Opacity, 60)
}, "PageDownloadInstall Visibility " + panel.Name);
else
ModAnimation.AniStart(
new[]
{
ModAnimation.AaTranslateY(panel, 6d - ((TranslateTransform)panel.RenderTransform).Y, 60),
ModAnimation.AaOpacity(panel, -panel.Opacity, 60)
}, "PageDownloadInstall Visibility " + panel.Name);
}
/// <summary>
/// 获取实例图标。
/// </summary>
private string GetSelectLogo()
{
if (selectedFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png";
if (selectedLegacyFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png";
if (selectedForge is not null) return "pack://application:,,,/images/Blocks/Anvil.png";
if (selectedNeoForge is not null) return "pack://application:,,,/images/Blocks/NeoForge.png";
if (selectedLiteLoader is not null) return "pack://application:,,,/images/Blocks/Egg.png";
if (selectedOptiFine is not null) return "pack://application:,,,/images/Blocks/GrassPath.png";
if (selectedCleanroom is not null) return "pack://application:,,,/images/Blocks/Cleanroom.png";
if (selectedLabyModVersion is not null) return "pack://application:,,,/images/Blocks/LabyMod.png";
return _vanillaIcon;
}
/// <summary>
/// 获取实例描述信息。
/// </summary>
private string GetSelectInfo()
{
var parts = new List<string>
{
_vanillaName
};
var loaderInfos = new (string NameKey, string? Version)[]
{
("Common.Installation.Fabric", selectedFabric?.Replace("+build", "")),
("Common.Installation.LegacyFabric", selectedLegacyFabric),
("Common.Installation.Forge", selectedForge?.VersionName),
("Common.Installation.NeoForge",
selectedNeoForge?.VersionName ?? VersionOrNull(selectedNeoForgeVersion)),
("Common.Installation.Cleanroom",
selectedCleanroom?.VersionName ?? VersionOrNull(selectedCleanroomVersion)),
("Common.Installation.LabyMod", selectedLabyModVersion),
("Common.Installation.OptiFine",
selectedOptiFine?.DisplayName.Replace(_vanillaName + " ", ""))
};
parts.AddRange(
loaderInfos
.Where(info => !string.IsNullOrWhiteSpace(info.Version))
.Select(info => $"{Lang.Text(info.NameKey)} {info.Version}")
);
if (selectedLiteLoader is not null) parts.Add(Lang.Text("Common.Installation.LiteLoader"));
if (parts.Count == 1) parts.Add(Lang.Text("Instance.Install.NoExtraInstall"));
return string.Join(" | ", parts);
}
private static string? VersionOrNull<T>(T version)
{
return EqualityComparer<T>.Default.Equals(version, default!)
? null
: version?.ToString();
}
#endregion
#region
private ModComp.CompFile _currentFabricApi; // 加载完成后直接调用以提高性能
private string _currentFabricApiPath;
private object GetCurrentFabricApi() // 进入页面和联网加载时调用
{
var loaderOutput = ModDownload.dlFabricApiLoader.output;
if (loaderOutput is null)
return null; // 确保联网信息已加载
var localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.McInstance,
new[] { "fabric-api", "fabric" }, "fabric", "api");
if (localComp is null)
return null;
var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? ""));
if (result is not null)
{
_currentFabricApi = result;
_currentFabricApiPath = localComp.path;
}
return result;
}
private ModComp.CompFile _currentLegacyFabricApi; // 加载完成后直接调用以提高性能
private string _currentLegacyFabricApiPath;
private object GetCurrentLegacyFabricApi() // 进入页面和联网加载时调用
{
var loaderOutput = ModDownload.dlLegacyFabricApiLoader.output;
if (loaderOutput is null)
return null; // 确保联网信息已加载
var localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.McInstance,
new[] { "legacy-fabric-api", "legacy-fabric" }, "legacy-fabric", "api");
if (localComp is null)
return null;
var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? ""));
if (result is not null)
{
_currentLegacyFabricApi = result;
_currentLegacyFabricApiPath = localComp.path;
}
return result;
}
private ModComp.CompFile _currentOptiFabric;
private string _currentOptiFabricPath;
private object GetCurrentOptiFabric()
{
var loaderOutput = ModDownload.dlOptiFabricLoader.output;
if (loaderOutput is null)
return null;
var localComp =
ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.McInstance, "optifabric", "optifabric", "opti");
if (localComp is null)
return null;
var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? ""));
if (result is not null)
{
_currentOptiFabric = result;
_currentOptiFabricPath = localComp.path;
}
return result;
}
// 当前信息获取
public void GetCurrentInfo()
{
ClearSelected();
BtnSelectStart.IsEnabled = true;
var currentInstance = PageInstanceLeft.McInstance.Info;
_vanillaName = currentInstance.VanillaName;
if (currentInstance.HasLiteLoader)
selectedLiteLoader = new ModDownload.DlLiteLoaderListEntry { Inherit = currentInstance.VanillaName };
if (currentInstance.HasOptiFine)
selectedOptiFine = new ModDownload.DlOptiFineListEntry
{
DisplayName = currentInstance.VanillaName + " " + currentInstance.OptiFine.Replace("_", " "),
IsPreview = currentInstance.OptiFine.ContainsF("pre"), Inherit = currentInstance.VanillaName,
NameVersion = currentInstance.VanillaName + "-OptiFine_HD_U_" + currentInstance.OptiFine
};
if (currentInstance.HasCleanroom)
{
selectedLoaderName = "Cleanroom";
selectedAPIName = "Cleanroom";
selectedCleanroomVersion = currentInstance.Cleanroom;
selectedCleanroom = new ModDownload.DlCleanroomListEntry(selectedCleanroomVersion);
}
else if (currentInstance.HasForge)
{
selectedLoaderName = "Forge";
selectedForge =
new ModDownload.DlForgeVersionEntry(currentInstance.Forge, null, currentInstance.VanillaName)
{
Category = "installer", forgeType = ModDownload.DlForgelikeEntry.ForgelikeType.Forge,
Inherit = currentInstance.VanillaName
};
}
else if (currentInstance.HasLegacyFabric)
{
selectedLoaderName = "LegacyFabric";
selectedLegacyFabric = currentInstance.LegacyFabric;
selectedLegacyFabricApi = (ModComp.CompFile)GetCurrentLegacyFabricApi();
}
else if (currentInstance.HasFabric)
{
selectedLoaderName = "Fabric";
selectedFabric = currentInstance.Fabric;
selectedFabricApi = (ModComp.CompFile)GetCurrentFabricApi();
}
else if (currentInstance.HasLabyMod)
{
selectedLoaderName = "LabyMod";
selectedLabyModVersion = currentInstance.LabyMod;
}
else if (currentInstance.HasNeoForge)
{
selectedLoaderName = "NeoForge";
selectedNeoForgeVersion = currentInstance.NeoForge;
selectedNeoForge = new ModDownload.DlNeoForgeListEntry(currentInstance.NeoForge)
{
VersionName = currentInstance.NeoForge, Inherit = currentInstance.VanillaName,
forgeType = ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge
};
}
if (currentInstance.HasFabric && currentInstance.HasOptiFine)
selectedOptiFabric = (ModComp.CompFile)GetCurrentOptiFabric();
_vanillaIcon = "pack://application:,,,/images/Blocks/Grass.png"; // TODO: 需要判断 Icon
currentInfo = GetSelectInfo();
EnterSelectPage();
}
private string currentInfo;
#endregion
#region
// 结果数据化
private static string GetVersionTypeTitle(string key) => key switch
{
"正式版" => Lang.Text("Download.Version.Type.Release"),
"预览版" => Lang.Text("Download.Version.Type.Development"),
"远古版" => Lang.Text("Download.Version.Type.BeforeRelease"),
"愚人节版" => Lang.Text("Download.Version.Type.AprilFools"),
_ => key
};
private void LoadMinecraft_OnFinish()
{
ExitSelectPage(); // 返回
do
{
try
{
var dict = new Dictionary<string, List<JsonObject>>
{
{ "正式版", new List<JsonObject>() }, { "预览版", new List<JsonObject>() }, { "远古版", new List<JsonObject>() },
{ "愚人节版", new List<JsonObject>() }
};
var versions = (JsonArray)ModDownload.dlClientListLoader.output.Value["versions"];
foreach (JsonObject Version in versions)
{
// 确定分类
var type = Version["type"].ToString();
var versionId = Version["id"].ToString().ToLower();
switch (type ?? "")
{
case "release":
{
type = "正式版";
break;
}
case "snapshot":
case "pending":
{
type = "预览版";
// Mojang 误分类
if (versionId.StartsWith("1.") && !versionId.Contains("combat") &&
!versionId.Contains("rc") && !versionId.Contains("experimental") &&
!versionId.Equals("1.2") && !versionId.Contains("pre"))
{
type = "正式版";
Version["type"] = "release";
}
// 愚人节版本
switch (Version["id"].ToString().ToLower() ?? "")
{
case "2point0_blue":
case "2point0_red":
case "2point0_purple":
case "2.0_blue":
case "2.0_red":
case "2.0_purple":
case "2.0":
{
type = "愚人节版";
Version["id"] = Version["id"].ToString().Replace("point", ".");
Version["type"] = "special";
Version.Add("lore", McVersionClassifier.GetMcFoolName((string)Version["id"]));
break;
}
case "20w14infinite":
case "20w14∞":
{
type = "愚人节版";
Version["id"] = "20w14∞";
Version["type"] = "special";
Version.Add("lore", McVersionClassifier.GetMcFoolName((string)Version["id"]));
break;
}
case "3d shareware v1.34":
case "1.rv-pre1":
case "15w14a":
case var @case when @case == "2.0":
case "22w13oneblockatatime":
case "23w13a_or_b":
case "24w14potato":
case "25w14craftmine":
case "26w14a":
{
type = "愚人节版";
Version["type"] = "special";
Version.Add("lore",
McVersionClassifier.GetMcFoolName((string)Version["id"])); // 4/1 自动视作愚人节版
break;
}
default:
{
var releaseDate = McVersionClassifier.GetReleaseTime(Version).ToUniversalTime().AddHours(2d);
if (releaseDate.Month == 4 && releaseDate.Day == 1)
{
type = "愚人节版";
Version["type"] = "special";
}
break;
}
}
break;
}
case "special":
{
// 已被处理的愚人节版
type = "愚人节版";
break;
}
default:
{
type = "远古版";
break;
}
}
// 加入辞典
dict[type].Add(Version);
}
// 排序
foreach (var Pair in dict.ToList())
dict[Pair.Key] = Pair.Value.OrderByDescending(McVersionClassifier.GetReleaseTime).ToList();
// 清空当前
PanMinecraft.Children.Clear();
// 添加最新版本
var cardInfo = new MyCard { Title = Lang.Text("Download.Version.Latest.Title"), Margin = new Thickness(0d, 15d, 0d, 15d) };
var topestVersions = new List<JsonObject>();
var release = (JsonObject)dict["正式版"][0].DeepClone();
release["lore"] = Lang.Text("Download.Version.Latest.Release", Lang.Date(release["releaseTime"].ToObject<DateTime>(), "g"));
topestVersions.Add(release);
if (dict["正式版"][0]["releaseTime"].ToObject<DateTime>() < dict["预览版"][0]["releaseTime"].ToObject<DateTime>())
{
var snapshot = (JsonObject)dict["预览版"][0].DeepClone();
snapshot["lore"] = Lang.Text("Download.Version.Latest.Development", Lang.Date(snapshot["releaseTime"].ToObject<DateTime>(), "g"));
topestVersions.Add(snapshot);
}
var panInfo = new StackPanel
{
Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d),
VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d),
Tag = topestVersions
};
void StackInstall(StackPanel stack)
{
foreach (var item in (IEnumerable)stack.Tag)
stack.Children.Add(ModDownloadLib.McDownloadListItem((JsonObject)item,
(sender, e) => MinecraftSelected((MyListItem)sender, e), false));
}
;
MyCard.StackInstall(ref panInfo, StackInstall);
cardInfo.Children.Add(panInfo);
PanMinecraft.Children.Insert(0, cardInfo);
// 添加其他版本
foreach (var Pair in dict)
{
if (!Pair.Value.Any())
continue;
// 增加卡片
var newCard = new MyCard
{ Title = GetVersionTypeTitle(Pair.Key) + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) };
var newStack = new StackPanel
{
Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d),
VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d),
Tag = Pair.Value
};
newCard.Children.Add(newStack);
newCard.SwapControl = newStack;
// 不能使用 AddressOf,这导致了 #535,原因完全不明,疑似是编译器 Bug
newCard.InstallMethod = StackInstall;
newCard.IsSwapped = true;
PanMinecraft.Children.Add(newCard);
}
// 自动选择版本
if (mcVersionWaitingForSelect is null)
break;
ModBase.Log("[Download] 自动选择 MC 版本:" + mcVersionWaitingForSelect);
foreach (JsonObject Version in versions)
{
if ((Version["id"].ToString() ?? "") != (mcVersionWaitingForSelect ?? ""))
continue;
var item = ModDownloadLib.McDownloadListItem(Version, (_, _) => { }, false);
MinecraftSelected(item, null);
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
} while (false);
}
/// <summary>
/// 当 MC 版本列表加载完时,立即自动选择的版本。用于外部调用。
/// </summary>
public static string mcVersionWaitingForSelect = null;
#endregion
#region OptiFine
/// <summary>
/// 获取 OptiFine 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadOptiFineGetError()
{
if (selectedLoaderName == "NeoForge" || selectedLoaderName == "LabyMod" || selectedLoaderName == "Cleanroom")
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
if (LoadOptiFine is null || LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Run)
return Lang.Text("Download.Install.State.Loading");
if (LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Error)
return $"{Lang.Text("Download.Install.State.GetVersionListFailed")}{((ModLoader.LoaderBase)LoadOptiFine.State).Error.Message}";
// 检查 Forge 1.13 - 1.14.3:全部不兼容
if (selectedLoaderName == "Forge" && McVersionComparer.CompareVersion(_vanillaName, "1.13") >= 0 &&
McVersionComparer.CompareVersion("1.14.3", _vanillaName) >= 0) return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
// 检查 Fabric 1.20.5+: 全部不兼容
if (selectedFabric is not null && McVersionComparer.CompareVersion(_vanillaName, "1.20.4") > 0)
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
// 检查 Loader
if (GetLoaderError(LoadOptiFine) is not null)
return GetLoaderError(LoadOptiFine);
// 检查 Forge 版本
var hasAny = false;
var hasRequiredVersion = false;
foreach (var OptiFineVersion in ModDownload.dlOptiFineListLoader.output.Value)
{
if (!OptiFineVersion.DisplayName.StartsWith(_vanillaName + " "))
continue; // 不是同一个大版本
hasAny = true;
if (selectedForge is null)
return null; // 未选择 Forge
if ((bool)IsOptiFineSuitForForge(OptiFineVersion, selectedForge))
return null; // 该版本可用
if (OptiFineVersion.RequiredForgeVersion is not null)
hasRequiredVersion = true;
}
if (!hasAny) return Lang.Text("Download.Install.State.NoVersion");
if (hasRequiredVersion) return Lang.Text("Download.Install.Compat.CompatForgeSpecificOnly");
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
}
// 检查某个 OptiFine 是否与某个 Forge 兼容
private object IsOptiFineSuitForForge(ModDownload.DlOptiFineListEntry optiFine,
ModDownload.DlForgeVersionEntry forge)
{
if ((forge.Inherit ?? "") != (optiFine.Inherit ?? ""))
return false; // 不是同一个大版本
if (optiFine.RequiredForgeVersion is null)
return false; // 不兼容 Forge
if (string.IsNullOrWhiteSpace(optiFine.RequiredForgeVersion))
return true; // #4183
if (optiFine.RequiredForgeVersion.Contains(".")) // XX.X.XXX
return McVersionComparer.CompareVersion(forge.version.ToString(), optiFine.RequiredForgeVersion) == 0;
// XXXX
return forge.version.Revision == Convert.ToDouble(optiFine.RequiredForgeVersion);
}
// 限制展开
private void CardOptiFine_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadOptiFineGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 OptiFine 版本列表。
/// </summary>
private void OptiFine_Loaded()
{
try
{
if (ModDownload.dlOptiFineListLoader.State != ModBase.LoadState.Finished)
return;
// 获取版本列表
var versions = new List<ModDownload.DlOptiFineListEntry>();
foreach (var Version in ModDownload.dlOptiFineListLoader.output.Value)
{
if (selectedForge is not null &&
!(bool)IsOptiFineSuitForForge(Version, selectedForge))
continue;
if (Version.DisplayName.StartsWith(_vanillaName + " "))
versions.Add(Version);
}
if (!versions.Any())
return;
// 排序
versions.Sort((left, right) =>
{
if (!left.IsPreview && right.IsPreview)
return true;
if (left.IsPreview && !right.IsPreview)
return false;
return McVersionComparer.CompareVersion(left.DisplayName, right.DisplayName) != 0;
});
// 可视化
PanOptiFine.Children.Clear();
foreach (var Version in versions)
PanOptiFine.Children.Add(
ModDownloadLib.OptiFineDownloadListItem(Version, (a, b) =>
this.OptiFine_Selected((dynamic)a, b), false));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 OptiFine 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void OptiFine_Selected(MyListItem sender, EventArgs e)
{
selectedOptiFine = (ModDownload.DlOptiFineListEntry)sender.Tag;
if (selectedForge is not null &&
!(bool)IsOptiFineSuitForForge(selectedOptiFine, selectedForge))
selectedForge = null;
OptiFabric_Loaded();
Forge_Loaded();
NeoForge_Loaded();
CardOptiFine.IsSwapped = true;
ReloadSelected();
}
private void OptiFine_Clear(object sender, MouseButtonEventArgs e)
{
selectedOptiFine = null;
selectedOptiFabric = null;
autoSelectedOptiFabric = false;
CardOptiFine.IsSwapped = true;
e.Handled = true;
Forge_Loaded();
NeoForge_Loaded();
ReloadSelected();
}
#endregion
#region LiteLoader
/// <summary>
/// 获取 LiteLoader 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadLiteLoaderGetError()
{
// 检查 Loader
if (GetLoaderError(LoadLiteLoader) is not null)
return GetLoaderError(LoadLiteLoader);
if (selectedLoaderName == "NeoForge" || selectedLoaderName == "LegacyFabric" || selectedLoaderName == "LabyMod" || selectedLoaderName == "Cleanroom")
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
// 检查版本
return ModDownload.dlLiteLoaderListLoader.output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? ""))
? null
: Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardLiteLoader_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadLiteLoaderGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 LiteLoader 版本列表。
/// </summary>
private void LiteLoader_Loaded()
{
try
{
if (ModDownload.dlLiteLoaderListLoader.State != ModBase.LoadState.Finished)
return;
// 获取版本列表
var versions = new List<ModDownload.DlLiteLoaderListEntry>();
foreach (var Version in ModDownload.dlLiteLoaderListLoader.output.Value)
if ((Version.Inherit ?? "") == (_vanillaName ?? ""))
versions.Add(Version);
if (!versions.Any())
return;
// 可视化
PanLiteLoader.Children.Clear();
foreach (var Version in versions)
PanLiteLoader.Children.Add(ModDownloadLib.LiteLoaderDownloadListItem(Version,
(a, b) => this.LiteLoader_Selected((dynamic)a, b), false));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 LiteLoader 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void LiteLoader_Selected(MyListItem sender, EventArgs e)
{
selectedLiteLoader = (ModDownload.DlLiteLoaderListEntry)sender.Tag;
CardLiteLoader.IsSwapped = true;
ReloadSelected();
}
private void LiteLoader_Clear(object sender, MouseButtonEventArgs e)
{
selectedLiteLoader = null;
CardLiteLoader.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region Forge
/// <summary>
/// 获取 Forge 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadForgeGetError()
{
if (McVersionComparer.CompareVersionGe("1.5.1", _vanillaName) && McVersionComparer.CompareVersionGe(_vanillaName, "1.1"))
return Lang.Text("Download.Install.State.NoVersion");
if (selectedLoaderName is not null && !ReferenceEquals(selectedLoaderName, "Forge"))
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
// 检查 Loader
if (GetLoaderError(LoadForge) is not null)
return GetLoaderError(LoadForge);
var loader = (ModLoader.LoaderTask<string, List<ModDownload.DlForgeVersionEntry>>)LoadForge.State;
if ((_vanillaName ?? "") != (loader.input ?? ""))
return Lang.Text("Download.Install.State.Getting");
// 检查版本
foreach (var Version in loader.output)
{
if (Version.Category == "universal" || Version.Category == "client")
continue; // 跳过无法自动安装的版本
if (selectedLoaderName is not null && selectedLoaderName != "Forge")
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
if (selectedOptiFine is not null && McVersionComparer.CompareVersionGe(_vanillaName, "1.13") &&
McVersionComparer.CompareVersionGe("1.14.3", _vanillaName))
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine"); // 1.13 ~ 1.14.3 OptiFine 检查
if (selectedOptiFine is not null && !(bool)IsOptiFineSuitForForge(selectedOptiFine, Version))
continue;
return null;
}
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine");
}
// 限制展开
private void CardForge_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadForgeGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 Forge 版本列表。
/// </summary>
private void Forge_Loaded()
{
try
{
if (!LoadForge.State.IsLoader)
return;
var loader = (ModLoader.LoaderTask<string, List<ModDownload.DlForgeVersionEntry>>)LoadForge.State;
if ((_vanillaName ?? "") != (loader.input ?? ""))
return;
if (loader.State != ModBase.LoadState.Finished)
return;
// 获取要显示的版本
var versions = loader.output.ToList(); // 复制数组,以免 Output 在实例化后变空
if (!loader.output.Any())
return;
PanForge.Children.Clear();
versions = versions.Where(v =>
{
if (v.Category == "universal" || v.Category == "client")
return false; // 跳过无法自动安装的版本
if (selectedOptiFine is not null &&
!(bool)IsOptiFineSuitForForge(selectedOptiFine, v))
return false;
return true;
}).OrderByDescending(v => v).ToList();
ModDownloadLib.ForgeDownloadListItemPreload(PanForge, versions,
(a, b) => this.Forge_Selected((dynamic)a, b), false);
foreach (var Version in versions)
PanForge.Children.Add(
ModDownloadLib.ForgeDownloadListItem(Version, (a, b) => this.Forge_Selected((dynamic)a, b), false));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 Forge 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void Forge_Selected(MyListItem sender, EventArgs e)
{
selectedForge = (ModDownload.DlForgeVersionEntry)sender.Tag;
selectedLoaderName = "Forge";
CardForge.IsSwapped = true;
if (selectedOptiFine is not null &&
!(bool)IsOptiFineSuitForForge(selectedOptiFine, selectedForge))
selectedOptiFine = null;
OptiFine_Loaded();
ReloadSelected();
}
private void Forge_Clear(object sender, MouseButtonEventArgs e)
{
selectedForge = null;
selectedLoaderName = null;
CardForge.IsSwapped = true;
e.Handled = true;
OptiFine_Loaded();
ReloadSelected();
}
#endregion
#region NeoForge
/// <summary>
/// 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadNeoForgeGetError()
{
if (selectedOptiFine is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine");
if (selectedLoaderName is not null && !ReferenceEquals(selectedLoaderName, "NeoForge"))
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
// 检查 Loader
if (GetLoaderError(LoadNeoForge) is not null)
return GetLoaderError(LoadNeoForge);
// 检查版本
return ModDownload.dlNeoForgeListLoader.output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? ""))
? null
: Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardNeoForge_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadNeoForgeGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 NeoForge 版本列表。
/// </summary>
private void NeoForge_Loaded()
{
try
{
// 获取版本列表
if (ModDownload.dlNeoForgeListLoader.State != ModBase.LoadState.Finished)
return;
var versions = ModDownload.dlNeoForgeListLoader.output.Value
.Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList();
if (!versions.Any())
return;
// 可视化
PanNeoForge.Children.Clear();
ModDownloadLib.NeoForgeDownloadListItemPreload(PanNeoForge, versions,
(a, b) => this.NeoForge_Selected((dynamic)a, b),
false);
foreach (var Version in versions)
PanNeoForge.Children.Add(
ModDownloadLib.NeoForgeDownloadListItem(Version, (a, b) => this.NeoForge_Selected((dynamic)a, b),
false));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 NeoForge 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void NeoForge_Selected(MyListItem sender, EventArgs e)
{
selectedNeoForge = (ModDownload.DlNeoForgeListEntry)sender.Tag;
selectedLoaderName = "NeoForge";
CardNeoForge.IsSwapped = true;
OptiFine_Loaded();
ReloadSelected();
}
private void NeoForge_Clear(object sender, MouseButtonEventArgs e)
{
selectedNeoForge = null;
selectedLoaderName = null;
CardNeoForge.IsSwapped = true;
e.Handled = true;
OptiFine_Loaded();
ReloadSelected();
}
#endregion
#region Cleanroom
/// <summary>
/// 获取 Cleanroom 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadCleanroomGetError()
{
if (!_vanillaName.StartsWith("1."))
return Lang.Text("Download.Install.State.NoAvailableVersion");
if (selectedOptiFine is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine");
if (selectedLoaderName is not null && selectedLoaderName != "Cleanroom")
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
if (selectedLiteLoader is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithLiteLoader");
// 检查 Loader
if (GetLoaderError(LoadCleanroom) is not null)
return GetLoaderError(LoadCleanroom);
// 检查版本
return ModDownload.dlCleanroomListLoader.output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? ""))
? null
: Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardCleanroom_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadCleanroomGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 Cleanroom 版本列表。
/// </summary>
private void Cleanroom_Loaded()
{
try
{
// 获取版本列表
if (ModDownload.dlCleanroomListLoader.State != ModBase.LoadState.Finished)
return;
var versions = ModDownload.dlCleanroomListLoader.output.Value
.Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList();
if (!versions.Any())
return;
// 可视化
PanCleanroom.Children.Clear();
ModDownloadLib.CleanroomDownloadListItemPreload(PanCleanroom, versions,
(a, b) => this.Cleanroom_Selected((dynamic)a, b), false);
foreach (var Version in versions)
PanCleanroom.Children.Add(
ModDownloadLib.CleanroomDownloadListItem(Version, (a, b) => this.Cleanroom_Selected((dynamic)a, b),
false));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 Cleanroom 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void Cleanroom_Selected(MyListItem sender, EventArgs e)
{
selectedCleanroom = (ModDownload.DlCleanroomListEntry)sender.Tag;
selectedLoaderName = "Cleanroom";
CardCleanroom.IsSwapped = true;
OptiFine_Loaded();
ReloadSelected();
}
private void Cleanroom_Clear(object sender, MouseButtonEventArgs e)
{
selectedCleanroom = null;
selectedCleanroomVersion = null;
selectedLoaderName = null;
selectedAPIName = null;
CardCleanroom.IsSwapped = true;
e.Handled = true;
OptiFine_Loaded();
ReloadSelected();
}
#endregion
#region Fabric
/// <summary>
/// 获取 Fabric 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadFabricGetError()
{
// 检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容
if (selectedOptiFine is not null && McVersionComparer.CompareVersionGe(_vanillaName, "1.20.5"))
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine");
// 检查 Loader
if (GetLoaderError(LoadFabric) is not null)
return GetLoaderError(LoadFabric);
// 检查版本
foreach (JsonObject version in ModDownload.dlFabricListLoader.output.Value["game"].AsArray())
if ((version["version"].ToString() ?? "") ==
(_vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") ?? ""))
{
if (selectedLoaderName is not null && !ReferenceEquals(selectedLoaderName, "Fabric"))
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
return null;
}
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadFabricGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 Fabric 版本列表。
/// </summary>
private void Fabric_Loaded()
{
try
{
if (ModDownload.dlFabricListLoader.State != ModBase.LoadState.Finished)
return;
// 获取版本列表
var versions = (JsonArray)ModDownload.dlFabricListLoader.output.Value["loader"];
if (!versions.Any())
return;
// 可视化
PanFabric.Children.Clear();
PanFabric.Tag = versions;
CardFabric.SwapControl = PanFabric;
CardFabric.InstallMethod = stack =>
{
foreach (var item in (IEnumerable)stack.Tag)
stack.Children.Add(
ModDownloadLib.FabricDownloadListItem((JsonObject)item,
(a, b) => this.Fabric_Selected((dynamic)a, b)));
};
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 Fabric 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
public void Fabric_Selected(MyListItem sender, EventArgs e)
{
selectedFabric = ((dynamic)sender.Tag)["version"].ToString();
selectedLoaderName = "Fabric";
FabricApi_Loaded();
OptiFabric_Loaded();
CardFabric.IsSwapped = true;
ReloadSelected();
}
private void Fabric_Clear(object sender, MouseButtonEventArgs e)
{
selectedFabric = null;
selectedFabricApi = null;
autoSelectedFabricApi = false;
selectedOptiFabric = null;
autoSelectedOptiFabric = false;
selectedLoaderName = null;
selectedAPIName = null;
CardFabric.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region Fabric API
/// <summary>
/// 判断某 Fabric API 是否适配当前选择的原版版本。
/// </summary>
public bool IsFabricApiCompatible(ModComp.CompFile fabricApi)
{
var fabricApiName = fabricApi.DisplayName;
try
{
if (fabricApiName is null || _vanillaName is null)
return false;
fabricApiName = fabricApiName.ToLower();
_vanillaName = _vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").ToLower();
if (fabricApiName.StartsWith("[" + _vanillaName + "]"))
return true;
if (!fabricApiName.Contains("/") || !fabricApiName.Contains("]"))
return false;
// 直接的判断(例如 1.18.1/22w03a
foreach (var part in fabricApiName.BeforeFirst("]").TrimStart('[').Split("/"))
if ((part ?? "") == (_vanillaName ?? ""))
return true;
// 将版本名分割语素(例如 1.16.4/5)
var lefts = fabricApiName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+");
var rights = _vanillaName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+");
// 对每段进行判断
var i = 0;
while (true)
{
// 两边均缺失,感觉是一个东西
if (lefts.Count - 1 < i && rights.Count - 1 < i)
return true;
// 确定两边是否一致
var leftValue = lefts.Count - 1 < i ? "-1" : lefts[i];
var rightValue = rights.Count - 1 < i ? "-1" : rights[i];
if (!leftValue.Contains("/"))
{
if ((leftValue ?? "") != (rightValue ?? ""))
return false;
}
// 左边存在斜杠
else if (!leftValue.Contains(rightValue))
{
return false;
}
i += 1;
}
return true;
}
catch (Exception ex)
{
ModBase.Log(ex, "判断 Fabric API 版本适配性出错(" + fabricApiName + ", " + _vanillaName + "");
return false;
}
}
/// <summary>
/// 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadFabricApiGetError()
{
// 检查 Loader
if (GetLoaderError(LoadFabricApi) is not null)
return GetLoaderError(LoadFabricApi);
if (ModDownload.dlFabricApiLoader.output is null)
return selectedFabric is null ? Lang.Text("Download.Install.Compat.RequiresFabric") : Lang.Text("Download.Install.State.Getting");
// 检查版本
if (ModDownload.dlFabricApiLoader.output.Any(f => IsFabricApiCompatible(f)))
return selectedFabric is null ? Lang.Text("Download.Install.Compat.RequiresFabric") : null;
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadFabricApiGetError() is not null)
e.handled = true;
}
private bool autoSelectedFabricApi;
/// <summary>
/// 尝试重新可视化 FabricApi 版本列表。
/// </summary>
private void FabricApi_Loaded()
{
try
{
if (ModDownload.dlFabricApiLoader.State != ModBase.LoadState.Finished)
return;
if (_vanillaName is null || selectedFabric is null)
return;
// 获取版本列表
var versions = new List<ModComp.CompFile>();
foreach (var version in ModDownload.dlFabricApiLoader.output)
if (IsFabricApiCompatible(version))
{
if (!version.DisplayName.StartsWith("["))
{
ModBase.Log("[Download] 已特判修改 Fabric API 显示名:" + version.DisplayName, ModBase.LogLevel.Debug);
version.DisplayName = "[" + _vanillaName + "] " + version.DisplayName;
}
versions.Add(version);
}
if (!versions.Any())
return;
versions = versions.OrderByDescending(v => v.ReleaseDate).ToList();
// 可视化
PanFabricApi.Children.Clear();
foreach (var version in versions)
{
if (!IsFabricApiCompatible(version))
continue;
PanFabricApi.Children.Add(
ModDownloadLib.FabricApiDownloadListItem(version,
(a, b) => this.FabricApi_Selected((dynamic)a, b)));
}
// 自动选择 Fabric API
if (!autoSelectedFabricApi)
{
autoSelectedFabricApi = true;
ModBase.Log($"[Download] 已自动选择 Fabric API{((MyListItem)PanFabricApi.Children[0]).Title}");
FabricApi_Selected((MyListItem)PanFabricApi.Children[0], null);
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 Fabric API 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void FabricApi_Selected(MyListItem sender, EventArgs e)
{
selectedFabricApi = (ModComp.CompFile)sender.Tag;
selectedAPIName = "Fabric API";
CardFabricApi.IsSwapped = true;
ReloadSelected();
}
private void FabricApi_Clear(object sender, MouseButtonEventArgs e)
{
selectedFabricApi = null;
selectedAPIName = null;
CardFabricApi.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region LegacyFabric
/// <summary>
/// 获取 LegacyFabric 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadLegacyFabricGetError()
{
if (LoadLegacyFabric is null || LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Run)
return Lang.Text("Download.Install.State.Loading");
if (LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Error)
return Lang.Text("Download.Install.State.GetVersionListFailed", ((ModLoader.LoaderBase)LoadLegacyFabric.State).Error.Message);
foreach (JsonObject Version in ModDownload.dlLegacyFabricListLoader.output.Value["game"].AsArray())
if ((Version["version"].ToString() ?? "") == (_vanillaName ?? ""))
{
if (selectedLiteLoader is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithLiteLoader");
if (selectedLoaderName is not null && !ReferenceEquals(selectedLoaderName, "LegacyFabric"))
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
return null;
}
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardLegacyFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadLegacyFabricGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 LegacyFabric 版本列表。
/// </summary>
private void LegacyFabric_Loaded()
{
try
{
if (ModDownload.dlLegacyFabricListLoader.State != ModBase.LoadState.Finished)
return;
// 获取版本列表
var versions = (JsonArray)ModDownload.dlLegacyFabricListLoader.output.Value["loader"];
if (!versions.Any())
return;
// 可视化
PanLegacyFabric.Children.Clear();
PanLegacyFabric.Tag = versions;
CardLegacyFabric.SwapControl = PanLegacyFabric;
CardLegacyFabric.InstallMethod = stack =>
{
foreach (var item in (IEnumerable)stack.Tag)
stack.Children.Add(ModDownloadLib.LegacyFabricDownloadListItem((JsonObject)item,
(a, b) => this.LegacyFabric_Selected((dynamic)a, b)));
};
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 LegacyFabric 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
public void LegacyFabric_Selected(MyListItem sender, EventArgs e)
{
selectedLegacyFabric = ((dynamic)sender.Tag)["version"].ToString();
selectedLoaderName = "LegacyFabric";
LegacyFabricApi_Loaded();
CardLegacyFabric.IsSwapped = true;
ReloadSelected();
}
private void LegacyFabric_Clear(object sender, MouseButtonEventArgs e)
{
selectedLegacyFabric = null;
selectedLegacyFabricApi = null;
autoSelectedLegacyFabricApi = false;
selectedLoaderName = null;
selectedAPIName = null;
CardLegacyFabric.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region Legacy Fabric API
/// <summary>
/// 从显示名判断该 API 是否与某版本适配。
/// </summary>
public static bool IsSuitableLegacyFabricApi(List<string> supportVersions, string minecraftVersion)
{
try
{
if (supportVersions.Contains(minecraftVersion)) return true;
return false;
}
catch (Exception ex)
{
ModBase.Log(ex, "判断 Legacy Fabric API 版本适配性出错(" + supportVersions + ", " + minecraftVersion + "");
return false;
}
}
/// <summary>
/// 获取 LegacyFabricApi 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadLegacyFabricApiGetError()
{
if (LoadLegacyFabricApi is null || LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Run)
return Lang.Text("Download.Install.State.Loading");
if (LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Error)
return Lang.Text("Download.Install.State.GetVersionListFailed", ((ModLoader.LoaderBase)LoadLegacyFabricApi.State).Error.Message);
if (selectedAPIName is not null && !ReferenceEquals(selectedAPIName, "Legacy Fabric API"))
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedAPIName);
if (ModDownload.dlLegacyFabricApiLoader.output is null)
{
if (selectedLegacyFabric is null)
return Lang.Text("Download.Install.Compat.RequiresLegacyFabric");
return Lang.Text("Download.Install.State.Loading");
}
foreach (var Version in ModDownload.dlLegacyFabricApiLoader.output)
{
if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName))
continue;
if (selectedLegacyFabric is null)
return Lang.Text("Download.Install.Compat.RequiresLegacyFabric");
return null;
}
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardLegacyFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadLegacyFabricApiGetError() is not null)
e.handled = true;
}
private bool autoSelectedLegacyFabricApi;
/// <summary>
/// 尝试重新可视化 LegacyFabricApi 版本列表。
/// </summary>
private void LegacyFabricApi_Loaded()
{
try
{
if (ModDownload.dlLegacyFabricApiLoader.State != ModBase.LoadState.Finished)
return;
if (_vanillaName is null || selectedLegacyFabric is null)
return;
// 获取版本列表
var versions = new List<ModComp.CompFile>();
foreach (var Version in ModDownload.dlLegacyFabricApiLoader.output)
if (IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName))
versions.Add(Version);
if (!versions.Any())
return;
versions = versions.OrderByDescending(v => v.ReleaseDate).ToList();
// 可视化
PanLegacyFabricApi.Children.Clear();
foreach (var Version in versions)
{
if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName))
continue;
PanLegacyFabricApi.Children.Add(
ModDownloadLib.LegacyFabricApiDownloadListItem(Version,
(a, b) => this.LegacyFabricApi_Selected((dynamic)a, b)));
}
// 自动选择 Legacy Fabric API
if (!autoSelectedLegacyFabricApi)
{
autoSelectedLegacyFabricApi = true;
ModBase.Log($"[Download] 已自动选择 Legacy Fabric API{((MyListItem)PanLegacyFabricApi.Children[0]).Title}");
LegacyFabricApi_Selected((MyListItem)PanLegacyFabricApi.Children[0], null);
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 Legacy Fabric API 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void LegacyFabricApi_Selected(MyListItem sender, EventArgs e)
{
selectedLegacyFabricApi = (ModComp.CompFile)sender.Tag;
selectedAPIName = "Legacy Fabric API";
CardLegacyFabricApi.IsSwapped = true;
ReloadSelected();
}
private void LegacyFabricApi_Clear(object sender, MouseButtonEventArgs e)
{
selectedLegacyFabricApi = null;
selectedAPIName = null;
CardLegacyFabricApi.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region OptiFabric
/// <summary>
/// 判断某 OptiFabric 是否适配当前选择的原版版本。
/// </summary>
private bool IsOptiFabricCompatible(ModComp.CompFile modFile)
{
try
{
if (_vanillaName is null)
return false;
return modFile.GameVersions.Contains(_vanillaName);
}
catch (Exception ex)
{
ModBase.Log(ex, "判断 OptiFabric 版本适配性出错(" + _vanillaName + "");
return false;
}
}
private bool autoSelectedOptiFabric;
/// <summary>
/// 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadOptiFabricGetError()
{
if (VanillaDrop >= 140 && VanillaDrop <= 150)
return Lang.Text("Download.Install.Compat.OptiFabricOriginsRequired");
// 检查 Loader
if (GetLoaderError(LoadOptiFabric) is not null)
return GetLoaderError(LoadOptiFabric);
// 检查版本
if (ModDownload.dlOptiFabricLoader.output is null)
{
if (selectedFabric is null && selectedOptiFine is null)
return Lang.Text("Download.Install.Compat.RequiresOptiFineAndFabric");
if (selectedFabric is null)
return Lang.Text("Download.Install.Compat.RequiresFabric");
if (selectedOptiFine is null)
return Lang.Text("Download.Install.Compat.RequiresOptiFine");
return Lang.Text("Download.Install.State.Getting");
}
foreach (var version in ModDownload.dlOptiFabricLoader.output)
{
if (!IsOptiFabricCompatible(version))
continue; // 2135#
if (selectedFabric is null && selectedOptiFine is null)
return Lang.Text("Download.Install.Compat.RequiresOptiFineAndFabric");
if (selectedFabric is null)
return Lang.Text("Download.Install.Compat.RequiresFabric");
if (selectedOptiFine is null)
return Lang.Text("Download.Install.Compat.RequiresOptiFine");
return null; // 通过检查
}
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardOptiFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadOptiFabricGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 OptiFabric 版本列表。
/// </summary>
private void OptiFabric_Loaded()
{
try
{
if (ModDownload.dlOptiFabricLoader.State != ModBase.LoadState.Finished)
return;
if (_vanillaName is null || selectedFabric is null || selectedOptiFine is null)
return;
// 获取版本列表
var versions = new List<ModComp.CompFile>();
foreach (var Version in ModDownload.dlOptiFabricLoader.output)
if (IsOptiFabricCompatible(Version))
versions.Add(Version);
if (!versions.Any())
return;
// 排序
versions = versions.OrderByDescending(v => v.ReleaseDate).ToList();
// 可视化
PanOptiFabric.Children.Clear();
foreach (var Version in versions)
{
if (!IsOptiFabricCompatible(Version))
continue;
PanOptiFabric.Children.Add(
ModDownloadLib.OptiFabricDownloadListItem(Version,
(a, b) => this.OptiFabric_Selected((dynamic)a, b)));
}
// 自动选择 OptiFabric
if (autoSelectedOptiFabric || (VanillaDrop >= 140 && VanillaDrop <= 150))
return; // 1.14~15 不自动选择
autoSelectedOptiFabric = true;
ModBase.Log($"[Download] 已自动选择 OptiFabric{((MyListItem)PanOptiFabric.Children[0]).Title}");
OptiFabric_Selected((MyListItem)PanOptiFabric.Children[0], null);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 OptiFabric 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
private void OptiFabric_Selected(MyListItem sender, EventArgs e)
{
selectedOptiFabric = (ModComp.CompFile)sender.Tag;
CardOptiFabric.IsSwapped = true;
ReloadSelected();
}
private void OptiFabric_Clear(object sender, MouseButtonEventArgs e)
{
selectedOptiFabric = null;
CardOptiFabric.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
#region LabyMod
/// <summary>
/// 获取 LabyMod 的加载异常信息。若正常则返回 Nothing。
/// </summary>
private string LoadLabyModGetError()
{
if (LoadLabyMod is null || LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run)
return Lang.Text("Download.Install.State.Loading");
if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Error)
return Lang.Text("Download.Install.State.GetVersionListFailed", ((ModLoader.LoaderBase)LoadLabyMod.State).Error.Message);
// 检查 Loader
if (GetLoaderError(LoadLabyMod) is not null)
return GetLoaderError(LoadLabyMod);
if (selectedOptiFine is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithOptiFine");
if (selectedLoaderName is not null && selectedLoaderName != "LabyMod")
return Lang.Text("Download.Install.Compat.IncompatibleWithLoader", selectedLoaderName);
if (selectedLiteLoader is not null)
return Lang.Text("Download.Install.Compat.IncompatibleWithLiteLoader");
foreach (JsonObject Version in ModDownload.dlLabyModListLoader.output.Value["production"]["minecraftVersions"].AsArray())
if ((Version["version"].ToString() ?? "") == (_vanillaName ?? ""))
return null;
foreach (JsonObject Version in ModDownload.dlLabyModListLoader.output.Value["snapshot"]["minecraftVersions"].AsArray())
if ((Version["version"].ToString() ?? "") == (_vanillaName ?? ""))
return null;
return Lang.Text("Download.Install.State.NoVersion");
}
// 限制展开
private void CardLabyMod_PreviewSwap(object sender, ModBase.RouteEventArgs e)
{
if (LoadLabyModGetError() is not null)
e.handled = true;
}
/// <summary>
/// 尝试重新可视化 LabyMod 版本列表。
/// </summary>
private void LabyMod_Loaded()
{
try
{
if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run)
return;
// 获取版本列表
var versions = ModDownload.dlLabyModListLoader.output.Value;
if (versions is null || versions["production"] is null || versions["snapshot"] is null)
return;
// 可视化
var processedVersions = new JsonArray();
foreach (JsonObject Production in versions["production"]["minecraftVersions"].AsArray())
if ((Production["version"].ToString() ?? "") == (_vanillaName ?? ""))
{
var productionVersion = new JsonObject();
productionVersion.Add("version", versions["production"]["labyModVersion"].ToString());
productionVersion.Add("channel", "production");
productionVersion.Add("commitReference", versions["production"]["commitReference"].ToString());
processedVersions.Add(productionVersion);
}
foreach (JsonObject Snapshot in versions["snapshot"]["minecraftVersions"].AsArray())
if ((Snapshot["version"].ToString() ?? "") == (_vanillaName ?? ""))
{
var snapshotVersion = new JsonObject();
snapshotVersion.Add("version", versions["snapshot"]["labyModVersion"].ToString());
snapshotVersion.Add("channel", "snapshot");
snapshotVersion.Add("commitReference", versions["snapshot"]["commitReference"].ToString());
processedVersions.Add(snapshotVersion);
}
// MyMsgBox(If(ProcessedVersions.ToString, "Nothing"))
PanLabyMod.Children.Clear();
PanLabyMod.Tag = processedVersions;
CardLabyMod.SwapControl = PanLabyMod;
CardLabyMod.InstallMethod = stack =>
{
foreach (JsonObject item in (IEnumerable)stack.Tag)
stack.Children.Add(
ModDownloadLib.LabyModDownloadListItem(item, (a, b) => this.LabyMod_Selected((dynamic)a, b)));
};
}
catch (Exception ex)
{
ModBase.Log(
ex,
"可视化 LabyMod 安装版本列表出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Install.Error.OperationFailed"));
}
}
// 选择与清除
public void LabyMod_Selected(MyListItem sender, EventArgs e)
{
selectedLabyModChannel = ((dynamic)sender.Tag)("channel").ToString();
selectedLabyModCommitRef = ((dynamic)sender.Tag)("commitReference").ToString();
selectedLabyModVersion =
((dynamic)sender.Tag)("version").ToString() + (selectedLabyModChannel == "snapshot" ? " " + Lang.Text("Download.Version.Type.Snapshot") : " " + Lang.Text("Download.Version.Type.Stable"));
selectedLoaderName = "LabyMod";
CardLabyMod.IsSwapped = true;
ReloadSelected();
}
private void LabyMod_Clear(object sender, MouseButtonEventArgs e)
{
selectedLabyModCommitRef = null;
selectedLabyModVersion = null;
selectedLabyModChannel = null;
if (selectedLoaderName == "LabyMod")
{
selectedLoaderName = null;
}
selectedAPIName = null;
CardLabyMod.IsSwapped = true;
e.Handled = true;
ReloadSelected();
}
#endregion
}
@@ -0,0 +1,176 @@
<local:MyPageLeft x:Class="PCL.PageInstanceLeft"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" AnimatedControl="{Binding ElementName=PanItem, Mode=OneWay}">
<local:MyScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
Style="{StaticResource ScrollViewerFullMargin}">
<StackPanel Margin="0,12,0,0" x:Name="PanItem">
<TextBlock Text="{DynamicResource Instance.Left.Game}" Margin="13,6,5,4" Opacity="0.6" FontSize="12" />
<local:MyListItem x:Name="ItemOverall" IsScaleAnimationEnabled="False" Checked="True" Type="RadioBox"
Check="PageCheck"
Tag="0" MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Overview}"
SvgIcon="lucide/layout-dashboard" />
<local:MyListItem x:Name="ItemSetup" IsScaleAnimationEnabled="False" Tag="1" MinPaddingRight="35"
Check="PageCheck"
Height="36" VerticalAlignment="Top" Title="{DynamicResource Instance.Left.Settings}"
Type="RadioBox"
SvgIcon="lucide/settings">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="1" ToolTip="{DynamicResource Common.Action.Initialize}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="Reset"
SvgIcon="lucide/rotate-ccw"
LogoScale="0.9" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemInstall" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="10"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Install}"
LogoScale="0.87"
SvgIcon="lucide/wrench">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="10" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
LogoScale="0.85" Click="RefreshButton_Click"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="2"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Export}"
SvgIcon="lucide/package">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="2" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<TextBlock x:Name="TextResource" Text="{DynamicResource Instance.Left.Resources}" Margin="13,6,5,4"
Opacity="0.6" FontSize="12" />
<local:MyListItem x:Name="ItemWorld" LogoScale="0.95" IsScaleAnimationEnabled="False" Type="RadioBox"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Saves}" Tag="3"
SvgIcon="lucide/globe">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="3" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemScreenshot" LogoScale="0.95" IsScaleAnimationEnabled="False" Type="RadioBox"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Screenshots}" Tag="4"
SvgIcon="lucide/image">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="4" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemMod" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="5"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Mod}"
SvgIcon="lucide/puzzle">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="5" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="6"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Mod}"
SvgIcon="lucide/puzzle" />
<local:MyListItem x:Name="ItemResourcePack" LogoScale="0.85" IsScaleAnimationEnabled="False"
Check="PageCheck"
Type="RadioBox" MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.ResourcePacks}"
Tag="7"
SvgIcon="lucide/layers">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="7" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemShader" LogoScale="1.15" IsScaleAnimationEnabled="False" Type="RadioBox"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Shaders}" Tag="8"
SvgIcon="lucide/sparkles">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="8" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemSchematic" LogoScale="0.85" IsScaleAnimationEnabled="False" Type="RadioBox"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Schematics}" Tag="9"
SvgIcon="lucide/file-box">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="9" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<local:MyListItem x:Name="ItemServer" LogoScale="0.85" IsScaleAnimationEnabled="False" Type="RadioBox"
Check="PageCheck"
MinPaddingRight="35" Height="36" VerticalAlignment="Top"
Title="{DynamicResource Instance.Left.Servers}" Tag="11"
SvgIcon="lucide/server">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="11" ToolTip="{DynamicResource Common.Action.Refresh}"
ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
</StackPanel>
</local:MyScrollViewer>
</local:MyPageLeft>
@@ -0,0 +1,339 @@
using System.Windows;
using System.Windows.Controls;
using PCL.Core.App;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageInstanceLeft : IRefreshable
{
/// <summary>
/// 当前显示设置的 MC 实例。
/// </summary>
public static McInstance McInstance = null;
public PageInstanceLeft()
{
InitializeComponent();
Loaded += (_, _) => RefreshModDisabled();
}
public void Refresh()
{
Refresh(ModMain.frmMain.PageCurrentSub);
}
public void RefreshModDisabled()
{
var hide = Config.Preference.Hide;
if (McInstance is not null && McInstance.Modable)
{
ItemMod.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceMod
? Visibility.Collapsed
: Visibility.Visible;
ItemModDisabled.Visibility = Visibility.Collapsed;
}
else
{
ItemMod.Visibility = Visibility.Collapsed;
ItemModDisabled.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceMod
? Visibility.Collapsed
: Visibility.Visible;
}
// 功能隐藏
if (!PageSetupUI.HiddenForceShow)
{
var disableCount = 0;
if (hide.InstanceSave)
disableCount += 1;
if (hide.InstanceScreenshot)
disableCount += 1;
if (hide.InstanceMod)
disableCount += 1;
if (hide.InstanceResourcePack)
disableCount += 1;
if (hide.InstanceShader)
disableCount += 1;
if (hide.InstanceSchematic)
disableCount += 1;
if (hide.InstanceServer)
disableCount += 1;
if (disableCount == 7)
TextResource.Visibility = Visibility.Collapsed;
else
TextResource.Visibility = Visibility.Visible;
}
else
{
TextResource.Visibility = Visibility.Visible;
}
ItemInstall.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceEdit
? Visibility.Collapsed
: Visibility.Visible;
ItemExport.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceExport
? Visibility.Collapsed
: Visibility.Visible;
ItemWorld.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceSave
? Visibility.Collapsed
: Visibility.Visible;
ItemScreenshot.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceScreenshot
? Visibility.Collapsed
: Visibility.Visible;
ItemResourcePack.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceResourcePack
? Visibility.Collapsed
: Visibility.Visible;
ItemShader.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceShader
? Visibility.Collapsed
: Visibility.Visible;
ItemSchematic.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceSchematic
? Visibility.Collapsed
: Visibility.Visible;
ItemServer.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceServer
? Visibility.Collapsed
: Visibility.Visible;
}
private void RefreshButton_Click(object sender, EventArgs e) // 由边栏按钮匿名调用
{
Refresh((FormMain.PageSubType)ModBase.Val(((MyIconButton)sender).Tag));
}
public void Refresh(FormMain.PageSubType subType)
{
switch (subType)
{
case FormMain.PageSubType.VersionMod:
{
PageInstanceCompResource.Refresh(ModComp.CompType.Mod);
break;
}
case FormMain.PageSubType.VersionScreenshot:
{
var ignore= PageInstanceScreenshot.RefreshAsync();
break;
}
case FormMain.PageSubType.VersionWorld:
{
PageInstanceSaves.Refresh();
break;
}
case FormMain.PageSubType.VersionResourcePack:
{
PageInstanceCompResource.Refresh(ModComp.CompType.ResourcePack);
break;
}
case FormMain.PageSubType.VersionShader:
{
PageInstanceCompResource.Refresh(ModComp.CompType.Shader);
break;
}
case FormMain.PageSubType.VersionSchematic:
{
PageInstanceCompResource.Refresh(ModComp.CompType.Schematic);
break;
}
case FormMain.PageSubType.VersionInstall:
{
ModDownload.dlClientListLoader.Start(isForceRestart: true);
ModDownload.dlOptiFineListLoader.Start(isForceRestart: true);
ModDownload.dlForgeListLoader.Start(isForceRestart: true);
ModDownload.dlNeoForgeListLoader.Start(isForceRestart: true);
ModDownload.dlLiteLoaderListLoader.Start(isForceRestart: true);
ModDownload.dlFabricListLoader.Start(isForceRestart: true);
ModDownload.dlFabricApiLoader.Start(isForceRestart: true);
ModDownload.dlOptiFabricLoader.Start(isForceRestart: true);
ModDownload.dlLabyModListLoader.Start(isForceRestart: true);
ItemInstall.Checked = true;
ModMain.frmInstanceInstall.GetCurrentInfo();
break;
}
case FormMain.PageSubType.VersionExport:
{
if (ModMain.frmInstanceExport is not null)
ModMain.frmInstanceExport.RefreshAll();
ItemExport.Checked = true;
break;
}
case FormMain.PageSubType.VersionServer:
{
if (ModMain.frmInstanceServer is not null)
ModMain.frmInstanceServer.RefreshServers();
ItemServer.Checked = true;
break;
}
}
}
public void Reset(object sender, EventArgs e)
{
if (ModMain.MyMsgBox(Lang.Text("Instance.Left.InitializeSettings.ConfirmMessage"),
Lang.Text("Instance.Left.InitializeSettings.ConfirmTitle"),
button2: Lang.Text("Common.Action.Cancel"),
isWarn: true)
== 1)
{
if (ModMain.frmInstanceSetup is null)
ModMain.frmInstanceSetup = new PageInstanceSetup();
ModMain.frmInstanceSetup.Reset();
ItemSetup.Checked = true;
}
}
#region
/// <summary>
/// 当前页面的编号。从 0 开始计算。
/// </summary>
public FormMain.PageSubType pageID = FormMain.PageSubType.Default;
/// <summary>
/// 勾选事件改变页面。
/// </summary>
private void PageCheck(object sender, ModBase.RouteEventArgs e)
{
if (sender is MyListItem item && item.Tag is not null)
PageChange((FormMain.PageSubType)ModBase.Val(item.Tag));
}
public object PageGet(FormMain.PageSubType id)
{
if ((int)id == -1)
id = pageID;
switch (id)
{
case FormMain.PageSubType.VersionOverall:
{
if (ModMain.frmInstanceOverall is null)
ModMain.frmInstanceOverall = new PageInstanceOverall();
return ModMain.frmInstanceOverall;
}
case FormMain.PageSubType.VersionMod:
{
if (ModMain.frmInstanceMod is null)
ModMain.frmInstanceMod = new PageInstanceCompResource(ModComp.CompType.Mod);
return ModMain.frmInstanceMod;
}
case FormMain.PageSubType.VersionModDisabled:
{
if (ModMain.frmInstanceModDisabled is null)
ModMain.frmInstanceModDisabled = new PageInstanceModDisabled();
return ModMain.frmInstanceModDisabled;
}
case FormMain.PageSubType.VersionSetup:
{
if (ModMain.frmInstanceSetup is null)
ModMain.frmInstanceSetup = new PageInstanceSetup();
return ModMain.frmInstanceSetup;
}
case FormMain.PageSubType.VersionWorld:
{
if (ModMain.frmInstanceSaves is null)
ModMain.frmInstanceSaves = new PageInstanceSaves();
return ModMain.frmInstanceSaves;
}
case FormMain.PageSubType.VersionScreenshot:
{
if (ModMain.frmInstanceScreenshot is null)
ModMain.frmInstanceScreenshot = new PageInstanceScreenshot();
return ModMain.frmInstanceScreenshot;
}
case FormMain.PageSubType.VersionResourcePack:
{
if (ModMain.frmInstanceResourcePack is null)
ModMain.frmInstanceResourcePack = new PageInstanceCompResource(ModComp.CompType.ResourcePack);
return ModMain.frmInstanceResourcePack;
}
case FormMain.PageSubType.VersionShader:
{
if (ModMain.frmInstanceShader is null)
ModMain.frmInstanceShader = new PageInstanceCompResource(ModComp.CompType.Shader);
return ModMain.frmInstanceShader;
}
case FormMain.PageSubType.VersionSchematic:
{
if (ModMain.frmInstanceSchematic is null)
ModMain.frmInstanceSchematic = new PageInstanceCompResource(ModComp.CompType.Schematic);
return ModMain.frmInstanceSchematic;
}
case FormMain.PageSubType.VersionInstall:
{
if (ModMain.frmInstanceInstall is null)
ModMain.frmInstanceInstall = new PageInstanceInstall();
return ModMain.frmInstanceInstall;
}
case FormMain.PageSubType.VersionExport:
{
if (ModMain.frmInstanceExport is null)
ModMain.frmInstanceExport = new PageInstanceExport();
return ModMain.frmInstanceExport;
}
case FormMain.PageSubType.VersionServer:
{
if (ModMain.frmInstanceServer is null)
ModMain.frmInstanceServer = new PageInstanceServer();
return ModMain.frmInstanceServer;
}
default:
{
throw new Exception("未知的实例设置子页面种类:" + (int)id);
}
}
}
/// <summary>
/// 切换现有页面。
/// </summary>
public void PageChange(FormMain.PageSubType id)
{
if (pageID == id)
return;
ModAnimation.AniControlEnabled += 1;
try
{
PageChangeRun((MyPageRight)PageGet(id));
pageID = id;
}
catch (Exception ex)
{
ModBase.Log(
ex,
"切换分页面失败(ID " + (int)id + "",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Error.OperationFailed"));
}
finally
{
ModAnimation.AniControlEnabled -= 1;
}
}
private static void PageChangeRun(MyPageRight target)
{
ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
if (target.Parent is not null)
target.SetValue(ContentPresenter.ContentProperty, null);
ModMain.frmMain.pageRight = target;
((MyPageRight)ModMain.frmMain.PanMainRight.Child).PageOnExit();
ModAnimation.AniStart(new[]
{
ModAnimation.AaCode(() =>
{
((MyPageRight)ModMain.frmMain.PanMainRight.Child).PageOnForceExit();
ModMain.frmMain.PanMainRight.Child = ModMain.frmMain.pageRight;
ModMain.frmMain.pageRight.Opacity = 0d;
}, 130),
ModAnimation.AaCode(() =>
{
// 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
ModMain.frmMain.pageRight.Opacity = 1d;
ModMain.frmMain.pageRight.PageOnEnter();
}, 30, true)
}, "PageLeft PageChange");
}
#endregion
}
@@ -0,0 +1,40 @@
<local:MyPageRight x:Class="PCL.PageInstanceModDisabled"
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">
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanMain">
<Grid Margin="20,17">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="4" Margin="0,0,0,9" HorizontalAlignment="Center"
Text="{DynamicResource Instance.Resource.Mod.Disabled.Title}" FontSize="19" UseLayoutRounding="True"
SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle Grid.Row="1" Grid.ColumnSpan="4" HorizontalAlignment="Stretch" Height="2"
Fill="{DynamicResource ColorBrush3}" />
<TextBlock Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5"
Text="{DynamicResource Instance.Resource.Mod.Disabled.Message}"
TextWrapping="Wrap" />
<local:MyButton Grid.Row="3" Grid.Column="1" Height="35" HorizontalAlignment="Center" x:Name="BtnDownload"
MinWidth="140" Text="{DynamicResource Instance.Resource.Mod.Disabled.GoDownload}"
Margin="10,10,10,0" Padding="13,0" ColorType="Highlight" />
<local:MyButton Grid.Row="3" Grid.Column="2" Height="35" HorizontalAlignment="Center" x:Name="BtnVersion"
MinWidth="140" Text="{DynamicResource Instance.Resource.Mod.Disabled.SelectInstance}"
Margin="10,10,10,0" Padding="13,0" />
</Grid>
</local:MyCard>
</local:MyPageRight>
@@ -0,0 +1,41 @@
using System.Windows;
using PCL.Core.App;
namespace PCL;
public partial class PageInstanceModDisabled
{
public PageInstanceModDisabled()
{
InitializeComponent();
BtnDownload.Click += BtnDownload_Click;
BtnVersion.Click += BtnVersion_Click;
BtnDownload.Loaded += BtnDownload_Loaded;
}
private void BtnDownload_Click(object sender, EventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall);
}
private void BtnVersion_Click(object sender, EventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType
.Launch); // 在实例选择页面选定实例的时候只会返回一层,因此如果不先锚定 Launch,在选择实例后会回退到实例设置的这个页面
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect);
}
public void BtnDownload_Loaded(object? sender = null, RoutedEventArgs? e = null)
{
var newVisibility =
(Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow) ||
(ModMain.frmSelectRight is not null && ModMain.frmSelectRight.showHidden)
? Visibility.Collapsed
: Visibility.Visible;
if (BtnDownload.Visibility != newVisibility)
{
BtnDownload.Visibility = newVisibility;
PanMain.TriggerForceResize();
}
}
}
@@ -0,0 +1,132 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceOverall"
PanScroll="{Binding ElementName=PanBack}" Grid.IsSharedSizeScope="True">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<StackPanel x:Name="PanMain" Margin="25,10">
<local:MyCard Margin="0,15" Title="">
<Grid Margin="10,7" Name="PanDisplayItem" />
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Instance.Overall.Info.Title}">
<StackPanel x:Name="PanInfo" Margin="15,40,25,5">
<local:MyLoading x:Name="LabInfoLoading" Text="{DynamicResource Instance.Overall.Info.Loading}" Margin="0,0,0,10" />
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Instance.Overall.Personalization.Title}" x:Name="PanDisplay">
<StackPanel Margin="25,40,25,15">
<Grid x:Name="PanDisplayIcon" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="{DynamicResource Instance.Overall.Icon.Title}" Margin="0,0,25,0" />
<local:MyComboBox Grid.Column="1" x:Name="ComboDisplayLogo">
<local:MyComboBoxItem Content="{DynamicResource Common.Option.Auto}" IsSelected="True" Tag="" />
<local:MyComboBoxItem Content="{DynamicResource Common.Option.Customize}" x:Name="ItemDisplayLogoCustom" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Cobblestone}"
Tag="pack://application:,,,/images/Blocks/CobbleStone.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.CommandBlock}"
Tag="pack://application:,,,/images/Blocks/CommandBlock.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.GoldBlock}" Tag="pack://application:,,,/images/Blocks/GoldBlock.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.GrassBlock}" Tag="pack://application:,,,/images/Blocks/Grass.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.DirtPath}" Tag="pack://application:,,,/images/Blocks/GrassPath.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Anvil}" Tag="pack://application:,,,/images/Blocks/Anvil.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.RedstoneBlock}"
Tag="pack://application:,,,/images/Blocks/RedstoneBlock.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.RedstoneLampOn}"
Tag="pack://application:,,,/images/Blocks/RedstoneLampOn.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.RedstoneLampOff}"
Tag="pack://application:,,,/images/Blocks/RedstoneLampOff.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Egg}" Tag="pack://application:,,,/images/Blocks/Egg.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Fabric}"
Tag="pack://application:,,,/images/Blocks/Fabric.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Quilt}"
Tag="pack://application:,,,/images/Blocks/Quilt.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.NeoForgeFox}"
Tag="pack://application:,,,/images/Blocks/NeoForge.png" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Icon.Cleanroom}"
Tag="pack://application:,,,/images/Blocks/Cleanroom.png" />
</local:MyComboBox>
<TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="{DynamicResource Instance.Overall.Category.Title}"
Margin="0,0,25,0" />
<local:MyComboBox Grid.Column="1" Grid.Row="2" x:Name="ComboDisplayType">
<local:MyComboBoxItem Content="{DynamicResource Common.Option.Auto}" IsSelected="True" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Category.Hidden}"
ToolTip="{DynamicResource Instance.Overall.Category.Hidden.ToolTip}" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Category.Modable}" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Category.Regular}" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Category.LessUsed}" />
<local:MyComboBoxItem Content="{DynamicResource Instance.Overall.Category.AprilFools}" />
</local:MyComboBox>
</Grid>
<Grid Margin="0,15,0,7" Height="35">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton x:Name="BtnDisplayRename" Text="{DynamicResource Instance.Overall.Rename}"
MinWidth="140" Padding="13,0"
Margin="0,0,20,0" />
<local:MyButton x:Name="BtnDisplayDesc" Text="{DynamicResource Instance.Overall.EditDescription}"
MinWidth="140" Padding="13,0"
Margin="0,0,20,0" Grid.Column="1" />
<local:MyButton x:Name="BtnDisplayStar" Text="{DynamicResource Instance.Overall.Favorite}"
MinWidth="140" Padding="13,0"
Margin="0,0,20,0" Grid.Column="2" />
</Grid>
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Instance.Overall.Shortcuts.Title}" x:Name="PanFolder">
<StackPanel Margin="25,40,25,15">
<Grid Margin="0,2,0,7" Height="35">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton x:Name="BtnFolderVersion" Text="{DynamicResource Instance.Overall.Shortcuts.InstanceFolder}" MinWidth="140" Padding="13,0"
Margin="0,0,20,0" />
<local:MyButton x:Name="BtnFolderSaves" Text="{DynamicResource Instance.Overall.Shortcuts.SavesFolder}" MinWidth="140" Padding="13,0"
Margin="0,0,20,0" Grid.Column="1" />
<local:MyButton x:Name="BtnFolderMods" Text="{DynamicResource Instance.Overall.Shortcuts.ModsFolder}" MinWidth="140" Padding="13,0"
Margin="0,0,20,0" Grid.Column="2" />
</Grid>
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Instance.Overall.Manage.Title}" x:Name="PanManage">
<StackPanel Margin="25,40,25,15">
<WrapPanel Margin="0,-5,-20,7">
<local:MyButton x:Name="BtnManageScript" Text="{DynamicResource Instance.Overall.Manage.ExportLaunchScript}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0" Height="35" />
<local:MyButton x:Name="BtnManageTest" Text="{DynamicResource Instance.Overall.Manage.TestGame}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0"
Height="35" />
<local:MyButton x:Name="BtnManageCheck" Text="{DynamicResource Instance.Overall.Manage.RepairFiles}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0"
ToolTip="{DynamicResource Instance.Overall.Manage.RepairFiles.ToolTip}"
Height="35" />
<local:MyButton x:Name="BtnManageRestore" Text="{DynamicResource Common.Action.Reset}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0"
ToolTip="{DynamicResource Instance.Overall.Manage.Reinstall.ToolTip}"
Height="35" />
<local:MyButton x:Name="BtnManageDelete" Text="{DynamicResource Instance.Overall.Manage.Delete}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0"
ColorType="Red" Height="35" />
<local:MyButton x:Name="BtnManagePatch" Text="{DynamicResource Instance.Overall.Manage.PatchCore}" MinWidth="140" Padding="13,0"
Margin="0,7,20,0"
Height="35" />
</WrapPanel>
</StackPanel>
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,842 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using FluentValidation;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App;
using PCL.Core.App.Configuration;
using PCL.Core.App.Configuration.Storage;
using PCL.Core.Minecraft;
using PCL.Core.UI;
using PCL.Core.Utils.Validate;
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageInstanceOverall
{
private ModLoader.LoaderCombo<int> instanceInfoLoader;
private bool isLoad;
public MyListItem itemVersion;
private MyCompItem modpackCompItem;
public PageInstanceOverall()
{
InitializeComponent();
Loaded += PageSetupLaunch_Loaded;
LabInfoLoading.Text = Lang.Text("Instance.Overall.Info.Loading");
// Handles
ComboDisplayType.SelectionChanged += ComboDisplayType_SelectionChanged;
BtnDisplayDesc.Click += BtnDisplayDesc_Click;
BtnDisplayRename.Click += BtnDisplayRename_Click;
ComboDisplayLogo.SelectionChanged += ComboDisplayLogo_SelectionChanged;
BtnDisplayStar.Click += BtnDisplayStar_Click;
BtnFolderVersion.Click += BtnFolderVersion_Click;
BtnFolderSaves.Click += BtnFolderSaves_Click;
BtnFolderMods.Click += BtnFolderMods_Click;
BtnManageScript.Click += BtnManageScript_Click;
BtnManageCheck.Click += BtnManageCheck_Click;
BtnManageRestore.Click += BtnManageRestore_Click;
BtnManageTest.Click += BtnManageTest_Click;
BtnManageDelete.Click += BtnManageDelete_Click;
BtnManagePatch.Click += BtnManagePatch_Click;
}
private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e)
{
// 重复加载部分
PanBack.ScrollToHome();
// 更新设置
ItemDisplayLogoCustom.Tag = @"PCL\Logo.png";
Reload();
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
PanDisplay.TriggerForceResize();
}
/// <summary>
/// 确保当前页面上的信息已正确显示。
/// </summary>
private void Reload()
{
ModAnimation.AniControlEnabled += 1;
var instance = PageInstanceLeft.McInstance;
// 刷新设置项目
ComboDisplayType.SelectedIndex = States.Instance.CardType[instance.PathInstance];
BtnDisplayStar.Text = instance.IsStar ? Lang.Text("Instance.Overall.Unfavorite") : Lang.Text("Instance.Overall.Favorite");
BtnFolderMods.Visibility = instance.Modable ? Visibility.Visible : Visibility.Collapsed;
// 刷新实例显示
PanDisplayItem.Children.Clear();
itemVersion = PageSelectRight.McVersionListItem(instance);
itemVersion.IsHitTestVisible = false;
PanDisplayItem.Children.Add(itemVersion);
ModMain.frmMain.PageNameRefresh();
// 刷新实例信息
GetInstanceInfo();
// 刷新实例图标
ComboDisplayLogo.SelectedIndex = 0;
var logo = States.Instance.LogoPath[instance.PathInstance];
var logoCustom = States.Instance.IsLogoCustom[instance.PathInstance];
if (logoCustom)
foreach (MyComboBoxItem Selection in ComboDisplayLogo.Items)
if (Equals(Selection.Tag, logo) ||
(Equals(Selection.Tag, @"PCL\Logo.png") &&
logo.EndsWith(@"PCL\Logo.png")))
{
ComboDisplayLogo.SelectedItem = Selection;
break;
}
ModAnimation.AniControlEnabled -= 1;
}
private void GetInstanceInfo()
{
modpackCompItem = null;
ModBase.RunInUi(() =>
{
PanInfo.Children.Clear();
PanInfo.Children.Add(new MyLoading { Text = Lang.Text("Instance.Overall.Info.Loading"), Margin = new Thickness(0d, 0d, 0d, 10d) });
});
var loaders = new List<ModLoader.LoaderBase>();
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Instance.Overall.Info.LoadModpackInfoTask"), _ =>
{
var modpackId = States.Instance.ModpackId[PageInstanceLeft.McInstance.PathInstance];
if (!string.IsNullOrWhiteSpace(modpackId))
{
var compProjects = ModComp.CompRequest.GetCompProjectsByIds(new List<string> { modpackId });
if (compProjects.Count > 0)
ModBase.RunInUi(() =>
{
modpackCompItem = compProjects.First().ToCompItem(false, false);
modpackCompItem.Tag = compProjects.First();
});
}
})
{
block = true
});
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Instance.Overall.Info.LoadInstanceInfoTask"), _ => ModBase.RunInUi(() =>
{
var instance = PageInstanceLeft.McInstance;
var instanceInfo = instance.Info;
List<MyListItem> items = [];
var launchCount = States.Instance.LaunchCount[instance.PathInstance];
if (launchCount == 0)
items.Add(new MyListItem
{
Title = Lang.Text("Instance.Overall.Info.LaunchCount.Title"), Info = Lang.Text("Instance.Overall.Info.LaunchCount.Never"), Logo = "pack://application:,,,/images/Blocks/RedstoneLampOff.png"
});
else
items.Add(new MyListItem
{
Title = Lang.Text("Instance.Overall.Info.LaunchCount.Title"),
Info = Lang.Text("Instance.Overall.Info.LaunchCount.Count", States.Instance.LaunchCount[instance.PathInstance]),
Logo = "pack://application:,,,/images/Blocks/RedstoneLampOn.png"
});
if (!string.IsNullOrWhiteSpace(States.Instance.ModpackVersion[instance.PathInstance]))
items.Add(new MyListItem
{
Title = Lang.Text("Instance.Overall.Info.ModpackVersion"), Info = States.Instance.ModpackVersion[instance.PathInstance],
Logo = "pack://application:,,,/images/Blocks/CommandBlock.png"
});
items.Add(new MyListItem
{
Title = "Minecraft", Info = instanceInfo.VanillaName,
Logo = "pack://application:,,,/images/Blocks/Grass.png"
});
if (instanceInfo.HasForge)
items.Add(new MyListItem
{
Title = "Forge", Info = instanceInfo.Forge, Logo = "pack://application:,,,/images/Blocks/Anvil.png"
});
if (instanceInfo.HasNeoForge)
items.Add(new MyListItem
{
Title = "NeoForge", Info = instanceInfo.NeoForge,
Logo = "pack://application:,,,/images/Blocks/NeoForge.png"
});
if (instanceInfo.HasCleanroom)
items.Add(new MyListItem
{
Title = "Cleanroom", Info = instanceInfo.Cleanroom,
Logo = "pack://application:,,,/images/Blocks/Cleanroom.png"
});
if (instanceInfo.HasFabric)
items.Add(new MyListItem
{
Title = "Fabric", Info = instanceInfo.Fabric,
Logo = "pack://application:,,,/images/Blocks/Fabric.png"
});
if (instanceInfo.HasQuilt)
items.Add(new MyListItem
{
Title = "Quilt", Info = instanceInfo.Quilt, Logo = "pack://application:,,,/images/Blocks/Quilt.png"
});
if (instanceInfo.HasOptiFine)
items.Add(new MyListItem
{
Title = "OptiFine", Info = instanceInfo.OptiFine,
Logo = "pack://application:,,,/images/Blocks/GrassPath.png"
});
if (instanceInfo.HasLiteLoader)
items.Add(new MyListItem
{ Title = "LiteLoader", Info = Lang.Text("Instance.Overall.Info.Installed"), Logo = "pack://application:,,,/images/Blocks/Egg.png" });
if (instanceInfo.HasLegacyFabric)
items.Add(new MyListItem
{
Title = "Legacy Fabric", Info = instanceInfo.LegacyFabric,
Logo = "pack://application:,,,/images/Blocks/Fabric.png"
});
if (instanceInfo.HasLabyMod)
items.Add(new MyListItem
{
Title = "LabyMod", Info = instanceInfo.LabyMod,
Logo = "pack://application:,,,/images/Blocks/LabyMod.png"
});
var wrapPanel = new WrapPanel { Margin = new Thickness(0, -5, -20, 7) };
foreach (var item in items)
{
wrapPanel.Children.Add(item);
wrapPanel.Children.Add(new TextBlock { Width = 2d });
}
PanInfo.Children.Clear();
if (modpackCompItem is not null)
{
PanInfo.Children.Add(modpackCompItem);
PanInfo.Children.Add(new TextBlock());
}
PanInfo.Children.Add(wrapPanel);
})));
instanceInfoLoader = new ModLoader.LoaderCombo<int>("Instance Info Loader", loaders) { show = false };
instanceInfoLoader.Start();
}
#region
// 实例分类
private void ComboDisplayType_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!(isLoad && ModAnimation.AniControlEnabled == 0))
return;
if (ComboDisplayType.SelectedIndex != 1)
{
// 改为不隐藏
try
{
// 若设置分类为可安装 Mod,则显示正常的 Mod 管理页面
States.Instance.CardType[PageInstanceLeft.McInstance.PathInstance] = ComboDisplayType.SelectedIndex;
PageInstanceLeft.McInstance.displayType = (McInstanceCardType)States.Instance.CardType[PageInstanceLeft.McInstance.PathInstance];
ModMain.frmInstanceLeft.RefreshModDisabled();
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"修改实例分类失败({PageInstanceLeft.McInstance.Name}",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
Reload(); // 更新 “打开 Mod 文件夹” 按钮
}
else
{
// 改为隐藏
try
{
if (!States.Hint.HideGameInstance)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Overall.Hide.ConfirmMessage"), Lang.Text("Instance.Overall.Hide.ConfirmTitle"), button2: Lang.Text("Common.Action.Cancel")) != 1)
{
ComboDisplayType.SelectedIndex = 0;
return;
}
States.Hint.HideGameInstance = true;
}
States.Instance.CardType[PageInstanceLeft.McInstance.PathInstance] =
(int)McInstanceCardType.Hidden;
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"隐藏实例 {PageInstanceLeft.McInstance.Name} 失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
}
// 更改描述
private void BtnDisplayDesc_Click(object sender, MouseButtonEventArgs e)
{
try
{
var oldInfo = States.Instance.CustomInfo[PageInstanceLeft.McInstance.PathInstance];
var newInfo = ModMain.MyMsgBoxInput(Lang.Text("Instance.Overall.Description.EditTitle"), Lang.Text("Instance.Overall.Description.EditMessage"), oldInfo,
[], Lang.Text("Instance.Overall.Description.Default"));
if (newInfo is not null && (oldInfo ?? "") != (newInfo ?? ""))
States.Instance.CustomInfo[PageInstanceLeft.McInstance.PathInstance] = newInfo;
PageInstanceLeft.McInstance = new McInstance(PageInstanceLeft.McInstance.Name).Load();
Reload();
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"实例 {PageInstanceLeft.McInstance.Name} 描述更改失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 重命名实例
private void BtnDisplayRename_Click(object sender, MouseButtonEventArgs e)
{
try
{
// 确认输入的新名称
var oldName = PageInstanceLeft.McInstance.Name;
var oldPath = PageInstanceLeft.McInstance.PathInstance;
// 修改此部分的同时修改快速安装的实例名检测*
var newName = ModMain.MyMsgBoxInput(Lang.Text("Instance.Overall.Name.EditTitle"), "", oldName,
[new FolderNameValidator(ModFolder.mcFolderSelected + "versions", ignoreCase: false)]);
if (string.IsNullOrWhiteSpace(newName))
return;
var newPath = Path.Combine(ModFolder.mcFolderSelected, "versions", newName);
// 获取临时中间名,以防止仅修改大小写的重命名失败
var tempName = newName + "_temp";
var tempPath = Path.Combine(ModFolder.mcFolderSelected, "versions", tempName);
var isCaseChangedOnly = (newName.ToLower() ?? "") == (oldName.ToLower() ?? "");
// 重新加载实例 Json 信息,避免 HMCL 项被合并
JsonObject jsonObject;
try
{
jsonObject = (JsonObject)ModBase.GetJson(ModBase.ReadFile(PageInstanceLeft.McInstance.PathInstance +
PageInstanceLeft.McInstance.Name + ".json"));
}
catch (Exception ex)
{
ModBase.Log(ex, "重命名读取 Json 时失败");
jsonObject = PageInstanceLeft.McInstance.JsonObject;
}
// 重命名主文件夹
FileSystem.RenameDirectory(oldPath, tempName);
FileSystem.RenameDirectory(tempPath, newName);
// 清理 ini 缓存
ModBase.IniClearCache(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "options.txt"));
// 重命名 Jar 文件与 natives 文件夹
// 不能进行遍历重命名,否则在实例名很短的时候容易误伤其他文件(Meloong-Git/#6443
if (Directory.Exists(Path.Combine(newPath, $"{oldName}-natives")))
{
if (isCaseChangedOnly)
{
FileSystem.RenameDirectory(Path.Combine(newPath, $"{oldName}-natives"), $"{oldName}natives_temp");
FileSystem.RenameDirectory(Path.Combine(newPath, $"{oldName}-natives_temp"), $"{newName}-natives");
}
else
{
ModBase.DeleteDirectory(Path.Combine(newPath, $"{newName}-natives"));
FileSystem.RenameDirectory(Path.Combine(newPath, $"{oldName}-natives"), $"{newName}-natives");
}
}
if (File.Exists(Path.Combine(newPath, $"{oldName}.jar")))
{
if (isCaseChangedOnly)
{
FileSystem.RenameFile(Path.Combine(newPath, $"{oldName}.jar"), $"{oldName}_temp.jar");
FileSystem.RenameFile(Path.Combine(newPath, $"{oldName}_temp.jar"), $"{newName}.jar");
}
else
{
File.Delete(Path.Combine(newPath, $"{newName}.jar"));
FileSystem.RenameFile(Path.Combine(newPath, $"{oldName}.jar"), $"{newName}.jar");
}
}
// 替换实例设置文件中的路径
if (File.Exists(Path.Combine(newPath, "PCL", "Setup.ini")))
ModBase.WriteFile(Path.Combine(newPath, "PCL", "Setup.ini"),
ModBase.ReadFile(Path.Combine(newPath, "PCL", "Setup.ini")).Replace(oldPath, newPath));
// 更改已选中的实例
if ((ModBase.ReadIni(ModFolder.mcFolderSelected + "PCL.ini", "Version") ?? "") == (oldName ?? ""))
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "Version", newName);
// 写入实例 Json,并删除旧的 Json
try
{
jsonObject["id"] = newName;
ModBase.WriteFile(Path.Combine(newPath, $"{newName}.json"), jsonObject.ToString());
if (!isCaseChangedOnly)
File.Delete(Path.Combine(newPath, $"{oldName}.json"));
}
catch (Exception ex)
{
ModBase.Log(ex, "重命名实例 Json 失败");
}
// 刷新与提示
HintService.Hint(Lang.Text("Instance.Overall.Name.RenameSuccess"), HintType.Success);
PageInstanceLeft.McInstance = new McInstance(newName).Load();
if (ModInstanceList.McMcInstanceSelected is not null &&
ModInstanceList.McMcInstanceSelected.Equals(PageInstanceLeft.McInstance))
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "Version", newName);
Reload();
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
"重命名实例失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 实例图标
private void ComboDisplayLogo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!(isLoad && ModAnimation.AniControlEnabled == 0))
return;
// 选择 自定义 时修改图片
try
{
if (ReferenceEquals(ComboDisplayLogo.SelectedItem, ItemDisplayLogoCustom))
{
var fileName = SystemDialogs.SelectFile(Lang.Text("Instance.Overall.Icon.SelectFile.Filter"), Lang.Text("Instance.Overall.Icon.SelectFile.Title"));
if (string.IsNullOrEmpty(fileName))
{
Reload(); // 还原选项
return;
}
ModBase.CopyFile(fileName, PageInstanceLeft.McInstance.PathInstance + @"PCL\Logo.png");
}
else
{
File.Delete(PageInstanceLeft.McInstance.PathInstance + @"PCL\Logo.png");
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"更改自定义实例图标失败({PageInstanceLeft.McInstance.Name}",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
// 进行更改
try
{
string newLogo = ((MyComboBoxItem)ComboDisplayLogo.SelectedItem).Tag?.ToString();
States.Instance.LogoPath[PageInstanceLeft.McInstance.PathInstance] = newLogo;
States.Instance.IsLogoCustom[PageInstanceLeft.McInstance.PathInstance] = !string.IsNullOrEmpty(newLogo);
// 刷新显示
ModBase.WriteIni(ModFolder.mcFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存
PageInstanceLeft.McInstance = new McInstance(PageInstanceLeft.McInstance.Name).Load();
Reload();
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"更改实例图标失败({PageInstanceLeft.McInstance.Name}",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 收藏夹
private void BtnDisplayStar_Click(object sender, MouseButtonEventArgs e)
{
try
{
States.Instance.Starred[PageInstanceLeft.McInstance.PathInstance] = !PageInstanceLeft.McInstance.IsStar;
PageInstanceLeft.McInstance = new McInstance(PageInstanceLeft.McInstance.Name).Load();
Reload();
ModInstanceList.mcInstanceListForceRefresh = true;
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"实例 {PageInstanceLeft.McInstance.Name} 收藏状态更改失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
#endregion
#region
// 实例文件夹
private void BtnFolderVersion_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
OpenVersionFolder(PageInstanceLeft.McInstance);
}
public static void OpenVersionFolder(McInstance version)
{
ModBase.OpenExplorer(version.PathInstance);
}
// 存档文件夹
private void BtnFolderSaves_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
var folderPath = PageInstanceLeft.McInstance.PathIndie + @"saves\";
Directory.CreateDirectory(folderPath);
ModBase.OpenExplorer(folderPath);
}
// Mod 文件夹
private void BtnFolderMods_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
var folderPath = PageInstanceLeft.McInstance.PathIndie + @"mods\";
Directory.CreateDirectory(folderPath);
ModBase.OpenExplorer(folderPath);
}
#endregion
#region
// 导出启动脚本
private void BtnManageScript_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
try
{
// 弹窗要求指定脚本的保存位置
var savePath = SystemDialogs.SelectSaveFile(Lang.Text("Instance.Overall.Script.SelectSaveTitle"), "启动 " + PageInstanceLeft.McInstance.Name + ".bat",
Lang.Text("Instance.Overall.Script.FileFilter"));
if (string.IsNullOrEmpty(savePath))
return;
// 检查中断(等玩家选完弹窗指不定任务就结束了呢……)
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading)
{
HintService.Hint(Lang.Text("Instance.Overall.Script.WaitForLaunchTask"), HintType.Error);
return;
}
// 生成脚本
if (ModLaunch.McLaunchStart(new ModLaunch.McLaunchOptions
{ SaveBatch = savePath, instance = PageInstanceLeft.McInstance }))
{
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Legacy)
HintService.Hint(Lang.Text("Instance.Overall.Script.Exporting"));
else
HintService.Hint(Lang.Text("Instance.Overall.Script.ExportingWarning"));
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"导出启动脚本失败({PageInstanceLeft.McInstance.Name}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 补全文件
private void BtnManageCheck_Click(object sender, MouseButtonEventArgs e)
{
try
{
// 忽略文件检查提示
if ((bool)ModLibrary.ShouldIgnoreFileCheck(PageInstanceLeft.McInstance))
{
HintService.Hint(Lang.Text("Instance.Overall.Repair.DisableVerificationHint"));
return;
}
// 重复任务检查
var taskName = PageInstanceLeft.McInstance.Name + " " + Lang.Text("Instance.Overall.Repair.TaskName");
foreach (var OngoingLoader in ModLoader.loaderTaskbar)
{
if ((OngoingLoader.name ?? "") != (taskName ?? ""))
continue;
HintService.Hint(Lang.Text("Instance.Overall.Repair.Processing"), HintType.Error);
return;
}
// 启动
var loader = new ModLoader.LoaderCombo<string>(taskName,
ModDownload.DlClientFix(PageInstanceLeft.McInstance, true,
ModDownload.AssetsIndexExistsBehaviour.AlwaysDownload));
loader.OnStateChanged = _ =>
{
switch (loader.State)
{
case ModBase.LoadState.Finished:
{
HintService.Hint(
Lang.Text("Instance.Overall.Repair.Success.WithTaskName", taskName), HintType.Success);
break;
}
case ModBase.LoadState.Failed:
{
HintService.Hint(
Lang.Text("Instance.Overall.Repair.Failed.WithDetail", taskName, loader.Error.ToString()),
HintType.Error);
break;
}
case ModBase.LoadState.Aborted:
{
HintService.Hint(
Lang.Text("Instance.Overall.Repair.Cancelled.WithTaskName", taskName));
break;
}
}
};
loader.Start(PageInstanceLeft.McInstance.Name);
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"尝试补全文件失败({PageInstanceLeft.McInstance.Name}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 重置
private void BtnManageRestore_Click(object sender, MouseButtonEventArgs e)
{
try
{
var currentVersion = PageInstanceLeft.McInstance.Info;
if (!(currentVersion.Drop == 99) &&
McVersionComparer.CompareVersion(currentVersion.VanillaName, "1.5.2") == -1 && currentVersion.HasForge)
{
HintService.Hint(Lang.Text("Instance.Overall.Reset.NotSupported"));
return;
}
if (currentVersion.HasQuilt)
{
HintService.Hint(Lang.Text("Instance.Overall.Reset.QuiltUnsupported"));
return;
}
// 确认操作
if (ModMain.MyMsgBox(
Lang.Text("Instance.Overall.Reset.ConfirmMessage", PageInstanceLeft.McInstance.Name), Lang.Text("Instance.Overall.Reset.ConfirmTitle"), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel")) == 2)
return;
// 备份实例核心文件
ModBase.CopyFile(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name + ".json",
PageInstanceLeft.McInstance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.McInstance.Name +
".json");
ModBase.CopyFile(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name + ".jar",
PageInstanceLeft.McInstance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.McInstance.Name +
".jar");
// 提交安装申请
var request = new ModDownloadLib.McInstallRequest
{
targetInstanceName = PageInstanceLeft.McInstance.Name,
targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{PageInstanceLeft.McInstance.Name}\",
minecraftName = currentVersion.VanillaName,
optiFineEntry = currentVersion.HasOptiFine
? new ModDownload.DlOptiFineListEntry
{
Inherit = currentVersion.VanillaName,
DisplayName = currentVersion.VanillaName + " " + currentVersion.OptiFine
}
: null,
forgeEntry = currentVersion.HasForge
? new ModDownload.DlForgeVersionEntry(currentVersion.Forge, null, currentVersion.VanillaName)
{ Category = "installer" }
: null,
forgeVersion = currentVersion.HasForge ? currentVersion.Forge : null,
neoForgeVersion = currentVersion.HasNeoForge ? currentVersion.NeoForge : null,
cleanroomVersion = currentVersion.HasCleanroom ? currentVersion.Cleanroom : null,
fabricVersion = currentVersion.HasFabric ? currentVersion.Fabric : null,
liteLoaderEntry = currentVersion.HasLiteLoader
? new ModDownload.DlLiteLoaderListEntry { Inherit = currentVersion.VanillaName }
: null,
legacyFabricVersion = currentVersion.HasLegacyFabric ? currentVersion.LegacyFabric : null
};
// .MinecraftJson = CurrentVersion.McName,
if (!ModDownloadLib.McInstall(request, Lang.Text("Common.Action.Reset")))
return;
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"重置实例 {PageInstanceLeft.McInstance.Name} 失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 测试游戏
private void BtnManageTest_Click(object sender, MouseButtonEventArgs e)
{
try
{
ModLaunch.McLaunchStart(new ModLaunch.McLaunchOptions
{ instance = PageInstanceLeft.McInstance, IsTest = true });
ModMain.frmMain.PageChange(FormMain.PageType.Launch);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"测试游戏失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 删除实例
// 修改此代码时,同时修改 PageSelectRight 中的代码
private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e)
{
try
{
var isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
var isIsolatedInstance =
PageInstanceLeft.McInstance.state != McInstanceState.Error &&
!string.Equals(
PageInstanceLeft.McInstance.PathIndie,
ModFolder.mcFolderSelected,
StringComparison.OrdinalIgnoreCase
);
var confirmMessageKey = (isIsolatedInstance, isShiftPressed) switch
{
(true, true) => "Instance.Overall.Delete.ConfirmMessageIsolatedPermanent",
(true, false) => "Instance.Overall.Delete.ConfirmMessageIsolated",
(false, true) => "Instance.Overall.Delete.ConfirmMessagePermanent",
(false, false) => "Instance.Overall.Delete.ConfirmMessage"
};
var confirmResult = ModMain.MyMsgBox(
Lang.Text(confirmMessageKey, PageInstanceLeft.McInstance.Name),
Lang.Text("Instance.Overall.Delete.ConfirmTitle"),
button2: Lang.Text("Common.Action.Cancel"),
isWarn: isIsolatedInstance || isShiftPressed
);
switch (confirmResult)
{
case 1:
{
var instancePath = PageInstanceLeft.McInstance.PathInstance;
var instanceName = PageInstanceLeft.McInstance.Name;
ModBase.IniClearCache(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "options.txt"));
((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache(
instancePath);
if (isShiftPressed)
{
ModBase.DeleteDirectory(instancePath);
HintService.Hint(Lang.Text("Instance.Overall.Delete.PermanentSuccess", instanceName),
HintType.Success);
}
else
{
FileSystem.DeleteDirectory(instancePath, UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);
HintService.Hint(Lang.Text("Instance.Overall.Delete.RecycleBinSuccess", instanceName),
HintType.Success);
}
break;
}
case 2:
{
return;
}
}
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\");
ModMain.frmMain.PageBack();
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "删除实例 " + PageInstanceLeft.McInstance.Name + " 被主动取消");
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"删除实例 {PageInstanceLeft.McInstance.Name} 失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Overall.Error.OperationFailed"));
}
}
// 修补核心
private void BtnManagePatch_Click(object sender, MouseButtonEventArgs e)
{
switch (ModMain.MyMsgBox(
Lang.Text("Instance.Overall.Patch.ConfirmMessage", PageInstanceLeft.McInstance.Name),
Lang.Text("Instance.Overall.Patch.ConfirmTitle"), button2: Lang.Text("Common.Action.Cancel")))
{
case 1:
{
var userInput = SystemDialogs.SelectFile(Lang.Text("Instance.Overall.Patch.SelectFile.Filter"), Lang.Text("Instance.Overall.Patch.SelectFile.Title"));
if (userInput is null || string.IsNullOrWhiteSpace(userInput))
return;
HintService.Hint(Lang.Text("Instance.Overall.Patch.Patching"));
ModBase.RunInNewThread(() =>
{
var core = new GameCore(PageInstanceLeft.McInstance.PathInstance + PageInstanceLeft.McInstance.Name +
".jar");
core.AddToCore(userInput);
HintService.Hint(Lang.Text("Instance.Overall.Patch.Success"), HintType.Success);
Config.Instance.DisableAssetVerifyV2[PageInstanceLeft.McInstance.PathInstance] = true;
});
break;
}
case 2:
{
return;
}
}
}
#endregion
}
@@ -0,0 +1,94 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceSaves"
PanScroll="{Binding ElementName=PanBack}" Grid.IsSharedSizeScope="True">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanNoWorld">
<Grid Margin="20,17">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="4" Margin="0,0,0,9" HorizontalAlignment="Center"
Text="{DynamicResource Instance.Saves.Empty.Title}" FontSize="19"
UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle Grid.Row="1" Grid.ColumnSpan="4" HorizontalAlignment="Stretch" Height="2"
Fill="{DynamicResource ColorBrush3}" />
<TextBlock Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5"
Text="{DynamicResource Instance.Saves.Empty.Message}"
TextWrapping="Wrap" HorizontalAlignment="Center" />
<local:MyButton Grid.Row="3" Grid.Column="1" Height="35" HorizontalAlignment="Center"
Click="BtnOpenFolder_Click"
MinWidth="140" Text="{DynamicResource Instance.Saves.OpenFolder}"
Margin="10,10,10,0"
Padding="13,0" ColorType="Highlight" />
<local:MyButton Grid.Row="3" Grid.Column="2" Height="35" HorizontalAlignment="Center"
Click="BtnDownloadNew_Click"
MinWidth="140" Text="{DynamicResource Instance.Resource.DownloadNew}"
Margin="10,10,10,0"
Padding="13,0" />
<local:MyButton Grid.Row="3" Grid.Column="3" Height="35" HorizontalAlignment="Center"
Click="BtnPaste_Click"
MinWidth="140" Text="{DynamicResource Instance.Saves.PasteFile}"
Margin="10,10,10,0"
Padding="13,0" />
</Grid>
</local:MyCard>
<StackPanel Orientation="Vertical" Margin="25,10,25,10" x:Name="PanContent">
<local:MySearchBox Margin="0,15" HintText="{DynamicResource Instance.Saves.SearchHint}"
x:Name="SearchBox" />
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Instance.Saves.QuickActions}">
<Grid Height="35" Margin="25,40,15,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton Grid.Column="0" MinWidth="140"
Text="{DynamicResource Instance.Saves.OpenFolder}" Padding="13,0"
Margin="0,0,20,0" Click="BtnOpenFolder_Click"
HorizontalAlignment="Left" ColorType="Highlight" />
<local:MyButton Grid.Column="1" MinWidth="140"
Text="{DynamicResource Instance.Resource.DownloadNew}" Padding="13,0"
Margin="0,0,20,0" Click="BtnDownloadNew_Click"
HorizontalAlignment="Left" />
<local:MyButton Grid.Column="2" MinWidth="140"
Text="{DynamicResource Instance.Saves.PasteFile}" Padding="13,0"
Margin="0,0,20,0" Click="BtnPaste_Click"
HorizontalAlignment="Left" />
</Grid>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" x:Name="PanListBack" VerticalAlignment="Top"
Title="{DynamicResource Instance.Saves.ListTitle}"
MinHeight="55">
<Grid>
<StackPanel Margin="0,13,15,0" Orientation="Horizontal" VerticalAlignment="Top"
HorizontalAlignment="Right" Height="28">
<local:MyIconTextButton x:Name="BtnSort" Text="{DynamicResource Instance.Saves.SortBy}"
SvgIcon="lucide/arrow-up-down" />
</StackPanel>
</Grid>
<StackPanel Margin="20,48,18,22" Name="PanList" VerticalAlignment="Top" />
</local:MyCard>
</StackPanel>
</Grid>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,573 @@
using System.Collections.Specialized;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App.Localization;
using PCL.Core.UI;
namespace PCL;
public partial class PageInstanceSaves : IRefreshable
{
private readonly DispatcherTimer fileSystemRefreshTimer;
private readonly DispatcherTimer searchTimer;
private FileSystemWatcher fileSystemWatcher;
private bool isLoad;
private object quickPlayFeature = false;
private List<string> saveFolders = new();
private string worldPath;
public PageInstanceSaves()
{
InitializeComponent();
fileSystemRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
Loaded += PageSetupLaunch_Loaded;
Unloaded += Page_Unloaded;
fileSystemRefreshTimer.Tick += FileSystemRefreshTimer_Tick;
searchTimer.Tick += SearchTimer_Tick;
SearchBox.TextChanged += SearchRun;
}
void IRefreshable.Refresh()
{
RefreshSelf();
}
private void RefreshSelf()
{
Refresh();
CheckQuickPlay();
}
public static void Refresh()
{
if (ModMain.frmInstanceSaves is not null)
ModMain.frmInstanceSaves.Reload();
ModMain.frmInstanceLeft.ItemWorld.Checked = true;
HintService.Hint(Lang.Text("Instance.Saves.Status.Refreshing"), log: false);
}
private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e)
{
// 重复加载部分
PanBack.ScrollToHome();
worldPath = PageInstanceLeft.McInstance.PathIndie + @"saves\";
if (!Directory.Exists(worldPath))
Directory.CreateDirectory(worldPath);
Reload();
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
CheckQuickPlay();
// 初始化文件系统监视器和排序按钮
SetupFileSystemWatcher();
BtnSort.Click += BtnSortClick;
SetSortMethod(_currentSortMethod);
}
private string GetFolderNameFromPath(string fullPath)
{
return string.IsNullOrEmpty(fullPath) ? "" :
fullPath.EndsWith(@"\") ? new DirectoryInfo(fullPath).Parent?.Name : new DirectoryInfo(fullPath).Name;
}
private string GetFileNameFromPath(string fullPath)
{
return Path.GetFileName(fullPath);
}
private void SetupFileSystemWatcher()
{
if (fileSystemWatcher is not null) fileSystemWatcher.Dispose();
// 确保目录存在
if (!Directory.Exists(worldPath))
Directory.CreateDirectory(worldPath);
fileSystemWatcher = new FileSystemWatcher();
fileSystemWatcher.Path = worldPath;
fileSystemWatcher.IncludeSubdirectories = false;
fileSystemWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.LastWrite;
fileSystemWatcher.Created += OnFileSystemChanged;
fileSystemWatcher.Deleted += OnFileSystemChanged;
fileSystemWatcher.Renamed += OnFileSystemChanged;
fileSystemWatcher.EnableRaisingEvents = true;
}
private void OnFileSystemChanged(object sender, FileSystemEventArgs e)
{
fileSystemRefreshTimer.Stop();
fileSystemRefreshTimer.Start();
}
private void FileSystemRefreshTimer_Tick(object sender, EventArgs e)
{
fileSystemRefreshTimer.Stop();
ModBase.RunInUi(() => Reload(), true);
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
if (fileSystemWatcher is not null)
{
fileSystemWatcher.Created -= OnFileSystemChanged;
fileSystemWatcher.Deleted -= OnFileSystemChanged;
fileSystemWatcher.Renamed -= OnFileSystemChanged;
fileSystemWatcher.Dispose();
fileSystemWatcher = null;
}
fileSystemRefreshTimer.Stop();
searchTimer.Stop();
}
/// <summary>
/// 确保当前页面上的信息已正确显示。
/// </summary>
public void Reload()
{
ModAnimation.AniControlEnabled += 1;
PanBack.ScrollToHome();
LoadFileList();
ModAnimation.AniControlEnabled -= 1;
}
private void RefreshUI()
{
try
{
if (IsSearching)
{
var resultCount = _searchResult is null ? 0 : _searchResult.Count;
PanListBack.Title = Lang.Text("Instance.Saves.SearchResultTitle", resultCount.ToString());
}
else
{
PanListBack.Title = Lang.Text("Instance.Saves.SaveListTitle", saveFolders.Count.ToString());
}
if (saveFolders.Count == 0)
{
PanNoWorld.Visibility = Visibility.Visible;
PanContent.Visibility = Visibility.Collapsed;
PanNoWorld.UpdateLayout();
}
else
{
PanNoWorld.Visibility = Visibility.Collapsed;
PanContent.Visibility = Visibility.Visible;
PanContent.UpdateLayout();
var showingSaves = (IsSearching ? _searchResult : saveFolders).ToList();
if (showingSaves.Any())
{
var sortMethod = GetSortMethod(_currentSortMethod);
showingSaves.Sort((a, b) => sortMethod(a, b));
}
ModAnimation.AniControlEnabled += 1;
PanList.Children.Clear();
foreach (var curFolder in showingSaves)
{
// 检查文件夹是否仍然存在
if (!Directory.Exists(curFolder)) continue;
var saveLogo = Path.Combine(curFolder, "icon.png");
var tmpCurFolder = curFolder;
if (File.Exists(saveLogo))
{
var target =
$@"{PageInstanceLeft.McInstance.PathInstance}PCL\ImgCache\{ModBase.GetStringMD5(saveLogo)}.png";
ModBase.CopyFile(saveLogo, target);
saveLogo = target;
}
else
{
saveLogo = ModBase.pathImage + "Icons/NoIcon.png";
}
var worldItem = new MyListItem
{
Logo = saveLogo,
Title = GetFolderNameFromPath(curFolder),
Info =
Lang.Text("Instance.Saves.CreationTime", Lang.Date(Directory.GetCreationTime(curFolder), "d"), Lang.Date(Directory.GetLastWriteTime(curFolder), "d")),
Type = MyListItem.CheckType.Clickable
};
worldItem.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
var btnOpen = new MyIconButton
{
SvgIcon = "lucide/folder-open",
ToolTip = Lang.Text("Common.Action.Open")
};
btnOpen.Click += (_, _) => ModBase.OpenExplorer(tmpCurFolder);
var btnDelete = new MyIconButton
{
SvgIcon = "lucide/trash-2",
ToolTip = Lang.Text("Common.Action.Delete")
};
btnDelete.Click += (_, _) =>
{
worldItem.IsEnabled = false;
worldItem.Info = Lang.Text("Instance.Saves.Deleting");
ModBase.RunInNewThread(() =>
{
try
{
FileSystem.DeleteDirectory(tmpCurFolder, UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);
HintService.Hint(Lang.Text("Instance.Saves.DeletedToRecycleBin"));
ModBase.RunInUiWait(() => RemoveItem(worldItem));
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.DeleteFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.DeleteFailed"));
ModBase.RunInUiWait(() => Reload());
}
});
};
var btnCopy = new MyIconButton
{
SvgIcon = "lucide/copy",
ToolTip = Lang.Text("Common.Action.Copy")
};
btnCopy.Click += (_, _) =>
{
try
{
if (Directory.Exists(tmpCurFolder))
{
Clipboard.SetFileDropList(new StringCollection { tmpCurFolder });
HintService.Hint(Lang.Text("Instance.Saves.CopiedToClipboard"));
HintService.Hint(Lang.Text("Instance.Saves.CopyPasteWarning"));
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.FolderNotFound"));
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.CopyFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.CopyFailed"));
}
};
var btnInfo = new MyIconButton
{
SvgIcon = "lucide/info",
ToolTip = Lang.Text("Instance.Saves.Details")
};
btnInfo.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
var btnLaunch = new MyIconButton
{
SvgIcon = "lucide/play",
ToolTip = Lang.Text("Instance.Saves.QuickPlay")
};
btnLaunch.Click += (_, _) =>
{
var worldName = GetFileNameFromPath(tmpCurFolder);
var launchOptions = new ModLaunch.McLaunchOptions
{
WorldName = worldName,
instance = PageInstanceLeft.McInstance
};
ModLaunch.McLaunchStart(launchOptions);
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
};
if ((bool)quickPlayFeature)
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo, btnLaunch };
else
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo };
PanList.Children.Add(worldItem);
}
ModAnimation.AniControlEnabled -= 1;
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.RefreshUiFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.RefreshUiFailed"));
}
}
private void CheckQuickPlay()
{
try
{
var cur = new ModLaunch.LaunchArgument(PageInstanceLeft.McInstance);
quickPlayFeature = cur.HasArguments("--quickPlaySingleplayer");
}
catch (Exception ex)
{
ModBase.Log(
ex,
"检查存档快捷启动失败",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
private void LoadFileList()
{
try
{
ModBase.Log("[World] 刷新存档文件");
saveFolders.Clear();
if (Directory.Exists(worldPath))
saveFolders = Directory.EnumerateDirectories(worldPath).ToList();
else
saveFolders = new List<string>();
if (ModBase.modeDebug)
ModBase.Log("[World] 共发现 " + saveFolders.Count + " 个存档文件夹", ModBase.LogLevel.Debug);
PanList.Children.Clear();
CheckQuickPlay();
if (ModBase.modeDebug)
{
if ((bool)quickPlayFeature)
ModBase.Log("[World] 该实例支持存档快捷启动", ModBase.LogLevel.Debug);
else
ModBase.Log("[World] 该实例不支持存档快捷启动", ModBase.LogLevel.Debug);
}
RefreshUI(); // 确保UI刷新
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.LoadListFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.LoadListFailed"));
}
}
private void RemoveItem(MyListItem item)
{
if (PanList.Children.IndexOf(item) == -1)
return;
PanList.Children.Remove(item);
RefreshUI();
}
private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e)
{
ModBase.OpenExplorer(worldPath);
}
private void BtnPaste_Click(object sender, MouseButtonEventArgs e)
{
var files = Clipboard.GetFileDropList();
var loaders = new List<ModLoader.LoaderBase>();
loaders.Add(new ModLoader.LoaderTask<int, int>("Copy saves", _ =>
{
var copied = 0;
foreach (var i in files)
try
{
if (Directory.Exists(i))
{
if (Directory.Exists(worldPath + GetFolderNameFromPath(i)))
{
HintService.Hint(Lang.Text("Instance.Saves.DuplicateFolder", GetFolderNameFromPath(i)));
}
else
{
ModBase.CopyDirectory(i, worldPath + GetFolderNameFromPath(i));
copied += 1;
}
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.SourceNotFolder"));
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.PasteFolderFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.PasteFolderFailed"));
}
if (copied > 0)
HintService.Hint(Lang.Text("Instance.Saves.PastedCount", copied.ToString()), HintType.Success);
ModBase.RunInUi(() => Reload());
}));
var loader = new ModLoader.LoaderCombo<int>($"{PageInstanceLeft.McInstance.Name} - {Lang.Text("Instance.Saves.CopySave")}", loaders)
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
loader.Start(1);
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
}
private void BtnDownloadNew_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadWorld);
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
}
#region
private SortMethod _currentSortMethod = SortMethod.FileName;
private List<string> _searchResult;
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text);
private enum SortMethod
{
FileName,
CreateTime,
ModifyTime
}
private string GetSortName(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return Lang.Text("Instance.Saves.SortFileName");
}
case SortMethod.CreateTime:
{
return Lang.Text("Instance.Saves.SortCreateTime");
}
case SortMethod.ModifyTime:
{
return Lang.Text("Instance.Saves.SortModifyTime");
}
default:
{
return Lang.Text("Instance.Saves.SortFileName");
}
}
}
private void SetSortMethod(SortMethod target)
{
_currentSortMethod = target;
BtnSort.Text = Lang.Text("Instance.Saves.SortBy", GetSortName(target));
RefreshUI();
}
private void BtnSortClick(object sender, EventArgs e)
{
var body = new ContextMenu();
foreach (SortMethod i in Enum.GetValues(typeof(SortMethod)))
{
var item = new MyMenuItem();
item.Header = GetSortName(i);
item.Click += (_, _) => SetSortMethod(i);
body.Items.Add(item);
}
body.PlacementTarget = (UIElement)sender;
body.Placement = PlacementMode.Bottom;
body.IsOpen = true;
}
private void SearchRun(object sender, EventArgs e)
{
searchTimer.Stop();
searchTimer.Start();
}
private void SearchTimer_Tick(object sender, EventArgs e)
{
searchTimer.Stop();
PerformSearch();
}
private void PerformSearch()
{
try
{
if (IsSearching)
{
var queryList = new List<ModBase.SearchEntry<string>>();
foreach (var saveFolder in saveFolders)
{
var folderName = GetFolderNameFromPath(saveFolder);
var searchSource = new List<ModBase.SearchSource>();
searchSource.Add(new ModBase.SearchSource(folderName, 1d));
queryList.Add(new ModBase.SearchEntry<string> { item = saveFolder, searchSource = searchSource });
}
_searchResult = ModBase.Search(queryList, SearchBox.Text, ModBase.MaxLocalSearchDepth, 0.35d).Select(r => r.item).ToList();
}
else
{
_searchResult = null;
}
RefreshUI();
}
catch (Exception ex)
{
ModBase.Log(ex, Lang.Text("Instance.Saves.SearchError"));
}
}
private Func<string, string, int> GetSortMethod(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
StringComparison.OrdinalIgnoreCase);
}
case SortMethod.CreateTime:
{
return (a, b) => Directory.GetCreationTime(b).CompareTo(Directory.GetCreationTime(a));
}
case SortMethod.ModifyTime:
{
return (a, b) => Directory.GetLastWriteTime(b).CompareTo(Directory.GetLastWriteTime(a));
}
default:
{
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
StringComparison.OrdinalIgnoreCase);
}
}
}
#endregion
}
@@ -0,0 +1,108 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceSavesDatapack"
PanScroll="{Binding ElementName=PanBack}">
<Grid>
<Grid x:Name="PanAllBack">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
x:Name="PanBack">
<StackPanel x:Name="PanMain" Margin="25,10,25,10" Grid.IsSharedSizeScope="True">
<local:MySearchBox Margin="0,15" HintText="{DynamicResource Instance.Resource.Search.Hint}" x:Name="SearchBox" />
<local:MyCard Margin="0,0,0,15" x:Name="PanManage">
<WrapPanel Margin="15,8,0,15">
<local:MyButton x:Name="BtnManageOpen" MinWidth="110" Text="{DynamicResource Common.Action.OpenFolder}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" ColorType="Highlight" />
<local:MyButton x:Name="BtnManageInstall" MinWidth="110" Text="{DynamicResource Instance.Resource.InstallFromFiles}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageDownload" MinWidth="110" Text="{DynamicResource Instance.Resource.Datapack.DownloadNew}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageSelectAll" MinWidth="110" Text="{DynamicResource Instance.Resource.SelectAll}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" />
<local:MyButton x:Name="BtnManageInfoExport" MinWidth="110" Text="{DynamicResource Instance.Resource.ExportInfo}" Padding="13,7"
Margin="0,7,15,0" HorizontalAlignment="Left" ToolTip="{DynamicResource Instance.Resource.Datapack.ExportInfo.ToolTip}" />
</WrapPanel>
</local:MyCard>
<local:MyCard x:Name="PanListBack" VerticalAlignment="Top" Opacity="0" Margin="0,0,0,14" Title=" "
MinHeight="55">
<StackPanel x:Name="PanFilter" Margin="15,13,0,0" Orientation="Horizontal"
VerticalAlignment="Top" HorizontalAlignment="Left" Height="28">
<local:MyRadioButton Tag="0" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.All}" x:Name="BtnFilterAll" Checked="True" />
<local:MyRadioButton Tag="1" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Enabled}" x:Name="BtnFilterEnabled" />
<local:MyRadioButton Tag="2" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Disabled}" x:Name="BtnFilterDisabled" />
<local:MyRadioButton Tag="3" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Updatable}" x:Name="BtnFilterCanUpdate" />
<local:MyRadioButton Tag="4" ColorType="Highlight" VerticalAlignment="Center" Margin="2,0"
Text="{DynamicResource Instance.Resource.Filter.Error}" x:Name="BtnFilterError" />
</StackPanel>
<StackPanel Margin="0,13,15,0" Orientation="Horizontal" VerticalAlignment="Top"
HorizontalAlignment="Right" Height="28">
<local:MyIconTextButton x:Name="BtnSort" Text="{DynamicResource Instance.Resource.Sort.Text}"
SvgIcon="lucide/arrow-up-down" />
</StackPanel>
<StackPanel Margin="20,48,18,22" Name="PanList" VerticalAlignment="Top" />
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanEmpty">
<StackPanel Margin="20,17">
<TextBlock x:Name="TxtEmptyTitle" Margin="0,0,0,9" HorizontalAlignment="Center" Text="{DynamicResource Instance.Resource.Datapack.Empty.Title}"
FontSize="19" UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle HorizontalAlignment="Stretch" Height="2" Fill="{DynamicResource ColorBrush3}" />
<TextBlock x:Name="TxtEmptyDescription" Margin="10,10,10,0"
Text="{DynamicResource Instance.Resource.Datapack.Empty.Description}" TextWrapping="Wrap" />
<WrapPanel Margin="10,20,10,5" HorizontalAlignment="Center" Orientation="Horizontal">
<local:MyButton Height="35" x:Name="BtnHintInstall" MinWidth="130" Text="{DynamicResource Instance.Resource.InstallFromFiles}" Margin="5,0"
Padding="12,0" ColorType="Highlight" />
<local:MyButton Height="35" x:Name="BtnHintDownload" MinWidth="130" Text="{DynamicResource Instance.Resource.Datapack.DownloadNew}" Margin="5,0"
Padding="12,0" />
<local:MyButton Height="35" x:Name="BtnHintOpen" MinWidth="130" Text="{DynamicResource Common.Action.OpenFolder}" Margin="5,0"
Padding="12,0" />
</WrapPanel>
</StackPanel>
</local:MyCard>
</Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True"
x:Name="PanLoad" UseAnimation="False">
<local:MyLoading Text="{DynamicResource Instance.Resource.Datapack.Loading}" Margin="20,20,20,17" x:Name="Load" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</local:MyCard>
<local:MyCard x:Name="CardSelect" Visibility="Collapsed" Opacity="0"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="25,25,25,0" UseAnimation="False">
<local:MyCard.RenderTransform>
<TranslateTransform x:Name="TransSelect" Y="-10" />
</local:MyCard.RenderTransform>
<TextBlock x:Name="LabSelect" Text="" HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="9" Foreground="{DynamicResource ColorBrush2}" />
<StackPanel Orientation="Horizontal" Margin="5,28,5,5">
<local:MyIconTextButton x:Name="BtnSelectUpdate" Text="{DynamicResource Instance.Resource.Update}"
LogoScale="1"
SvgIcon="lucide/upload" />
<local:MyIconTextButton x:Name="BtnSelectEnable" Text="{DynamicResource Instance.Resource.Enable}"
LogoScale="1.05"
SvgIcon="lucide/circle-check" />
<local:MyIconTextButton x:Name="BtnSelectDisable" Text="{DynamicResource Instance.Resource.Disable}"
LogoScale="1"
SvgIcon="lucide/circle-minus" />
<local:MyIconTextButton x:Name="BtnSelectFavorites" Text="{DynamicResource Instance.Resource.Favorite}"
LogoScale="1"
SvgIcon="lucide/heart" />
<local:MyIconTextButton x:Name="BtnSelectShare" Text="{DynamicResource Instance.Resource.ShareSelected}"
LogoScale="1"
SvgIcon="lucide/share-2" />
<local:MyIconTextButton x:Name="BtnSelectDelete" Text="{DynamicResource Common.Action.Delete}"
LogoScale="0.96"
SvgIcon="lucide/trash-2" />
<local:MyIconTextButton x:Name="BtnSelectCancel" Text="{DynamicResource Instance.Resource.CancelSelection}"
LogoScale="0.8"
SvgIcon="lucide/x" />
</StackPanel>
</local:MyCard>
</Grid>
</local:MyPageRight>
@@ -0,0 +1,1698 @@
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App;
using PCL.Core.UI;
using PCL.Core.UI.Theme;
using PCL.Network;
using PCL.Network.Loaders;
using FileSystem = Microsoft.VisualBasic.FileSystem;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageInstanceSavesDatapack : IRefreshable
{
#region
private readonly Dictionary<string, (DateTime CreationTime, long Length)> datapackFileInfoCache = new();
// 获取数据包信息(带缓存)
private (DateTime CreationTime, long Length) GetDatapackFileInfo(string path)
{
(DateTime CreationTime, long Length) cacheItem;
if (datapackFileInfoCache.TryGetValue(path, out cacheItem)) return cacheItem;
try
{
var fileInfo = new FileInfo(path);
var newItem = (fileInfo.CreationTime, fileInfo.Length);
if (!datapackFileInfoCache.ContainsKey(path)) datapackFileInfoCache.Add(path, newItem);
return newItem;
}
catch (Exception ex)
{
ModBase.Log(ex, "获取数据包信息失败: " + path);
return (DateTime.MinValue, 0L);
}
}
// 页面关闭时清理缓存
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
datapackFileInfoCache.Clear();
}
#endregion
#region
private readonly MyLocalCompItem.SwipeSelect currentSwipSelect;
public PageInstanceSavesDatapack()
{
currentSwipSelect = new MyLocalCompItem.SwipeSelect { TargetFrm = this };
InitializeComponent();
Unloaded += Page_Unloaded;
Loaded += (_, _) => PageOther_Loaded();
LoaderInit();
PageExit += UnselectedAllWithAnimation;
// Handles
Load.Click += Load_Click;
BtnManageOpen.Click += BtnManageOpen_Click;
BtnHintOpen.Click += BtnManageOpen_Click;
BtnManageSelectAll.Click += BtnManageSelectAll_Click;
BtnManageInstall.Click += BtnManageInstall_Click;
BtnHintInstall.Click += BtnManageInstall_Click;
BtnManageDownload.Click += BtnManageDownload_Click;
BtnHintDownload.Click += BtnManageDownload_Click;
BtnManageInfoExport.Click += BtnManageInfoExport_Click;
Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation();
SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown;
BtnFilterAll.Check += ChangeFilter;
BtnFilterCanUpdate.Check += ChangeFilter;
BtnFilterDisabled.Check += ChangeFilter;
BtnFilterEnabled.Check += ChangeFilter;
BtnFilterError.Check += ChangeFilter;
BtnSort.Click += BtnSortClick;
BtnSelectEnable.Click += BtnSelectEnable_Click;
BtnSelectDisable.Click += BtnSelectDisable_Click;
BtnSelectUpdate.Click += BtnSelectUpdate_Click;
BtnSelectDelete.Click += BtnSelectDelete_Click;
BtnSelectCancel.Click += BtnSelectCancel_Click;
BtnSelectFavorites.Click += BtnSelectFavorites_Click;
BtnSelectShare.Click += BtnSelectShare_Click;
SearchBox.TextChanged += SearchRun;
}
private ModLocalComp.CompLocalLoaderData GetRequireLoaderData()
{
var res = new ModLocalComp.CompLocalLoaderData();
res.gameVersion = PageInstanceLeft.McInstance;
res.frm = null;
res.loaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList();
res.compPath = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
res.compType = ModComp.CompType.DataPack;
return res;
}
private bool isLoad;
public void PageOther_Loaded()
{
if (ModMain.frmMain.pageLast.page != FormMain.PageType.CompDetail)
PanBack.ScrollToHome();
ModAnimation.AniControlEnabled += 1;
selectedDatapacks.Clear();
ReloadDatapackFileList();
ChangeAllSelected(false);
ModAnimation.AniControlEnabled -= 1;
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
ModMain.frmMain.KeyDown += FrmMain_KeyDown;
// 调整按钮边距(这玩意儿没法从 XAML 改)
foreach (MyRadioButton Btn in PanFilter.Children)
Btn.LabText.Margin = new Thickness(-2, 0d, 8d, 0d);
}
/// <summary>
/// 刷新数据包列表。
/// </summary>
public void ReloadDatapackFileList(bool forceReload = false)
{
if (LoaderRun(forceReload
? ModLoader.LoaderFolderRunType.ForceRun
: ModLoader.LoaderFolderRunType.RunOnUpdated))
{
ModBase.Log("[System] 已刷新数据包列表");
datapackFileInfoCache.Clear();
ModBase.RunInUi(() =>
{
Filter = FilterType.All;
PanBack.ScrollToHome();
SearchBox.Text = "";
});
}
}
// 强制刷新
private void RefreshSelf()
{
Refresh();
}
void IRefreshable.Refresh()
{
RefreshSelf();
}
public void Refresh()
{
ModMain.frmInstanceSavesDatapack.ReloadDatapackFileList(true);
ModBase.Log("[Datapack] 刷新数据包列表");
}
private void LoaderInit()
{
PageLoaderInit(Load, PanLoad, PanAllBack, null, ModLocalComp.compResourceListLoader,
_ => LoadUIFromLoaderOutput(), () => ModComp.CompType.DataPack, false);
}
private void Load_Click(object sender, MouseButtonEventArgs e)
{
if (ModLocalComp.compResourceListLoader.State == ModBase.LoadState.Failed)
LoaderRun(ModLoader.LoaderFolderRunType.ForceRun);
}
public bool LoaderRun(ModLoader.LoaderFolderRunType type)
{
var loadPath = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
return ModLoader.LoaderFolderRun(ModLocalComp.compResourceListLoader, loadPath, type,
loaderInput: GetRequireLoaderData());
}
#endregion
#region UI
/// <summary>
/// 已加载的数据包 UI 缓存。Key 为数据包的 RawPath。
/// </summary>
public Dictionary<string, MyLocalCompItem> datapackItems = new();
/// <summary>
/// 将加载器结果的数据包列表加载为 UI。
/// </summary>
private void LoadUIFromLoaderOutput()
{
try
{
// 判断应该显示哪一个页面
if (ModLocalComp.compResourceListLoader.output.Any())
{
PanBack.Visibility = Visibility.Visible;
PanEmpty.Visibility = Visibility.Collapsed;
}
else
{
// 根据组件类型设置 PanEmpty 的文本内容
TxtEmptyTitle.Text = Lang.Text("Instance.Resource.Datapack.Empty.Title");
TxtEmptyDescription.Text = Lang.Text("Instance.Resource.Datapack.Empty.Description");
PanEmpty.Visibility = Visibility.Visible;
PanBack.Visibility = Visibility.Collapsed;
return;
}
// 修改缓存
datapackItems.Clear();
var itemsToShow = ModLocalComp.compResourceListLoader.output.ToList();
foreach (var DatapackEntity in itemsToShow)
datapackItems[DatapackEntity.RawPath] = BuildLocalCompItem(DatapackEntity);
// 显示结果
ModBase.RunInUi(() =>
{
Filter = FilterType.All;
SearchBox.Text = ""; // 这会触发结果刷新,所以需要在 DatapackItems 更新之后
RefreshUI();
SetSortMethod(SortMethod.CompName);
});
}
catch (Exception ex)
{
ModBase.Log(
ex,
"加载数据包列表 UI 失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
private MyLocalCompItem BuildLocalCompItem(ModLocalComp.LocalCompFile entry)
{
try
{
ModAnimation.AniControlEnabled += 1;
var newItem = new MyLocalCompItem
{
SnapsToDevicePixels = true,
Entry = entry,
buttonHandler = BuildLocalCompItemBtnHandler,
Checked = selectedDatapacks.Contains(entry.RawPath)
};
newItem.CurrentSwipe = currentSwipSelect;
newItem.Tags = entry.Tags;
entry.OnCompUpdate += _ => newItem.Refresh();
newItem.Refresh();
ModAnimation.AniControlEnabled -= 1;
return newItem;
}
catch (Exception ex)
{
ModAnimation.AniControlEnabled -= 1;
ModBase.Log(ex, $"创建 UI 项失败:{entry.RawPath}");
throw;
}
}
private void BuildLocalCompItemBtnHandler(MyLocalCompItem sender, EventArgs e)
{
// 点击事件
sender.Changed += (ss, e) => CheckChanged((MyLocalCompItem)ss, e);
// 文件项的点击事件:切换选中状态
sender.Click += (ss, e) =>
{
var s = (MyLocalCompItem)ss;
s.Checked = !s.Checked;
};
// 图标按钮
var btnOpen = new MyIconButton { LogoScale = 1.05d, SvgIcon = "lucide/folder-open", Tag = sender };
btnOpen.ToolTip = Lang.Text("Instance.Saves.OpenFileLocation");
ToolTipService.SetPlacement(btnOpen, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnOpen, 30d);
ToolTipService.SetHorizontalOffset(btnOpen, 2d);
btnOpen.Click += (sender, e) => Open_Click((MyIconButton)sender, e);
var btnCont = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/info", Tag = sender };
btnCont.ToolTip = Lang.Text("Instance.Saves.Detail");
ToolTipService.SetPlacement(btnCont, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnCont, 30d);
ToolTipService.SetHorizontalOffset(btnCont, 2d);
btnCont.Click += Info_Click;
sender.MouseRightButtonUp += Info_Click;
var btnDelete = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/trash-2", Tag = sender };
btnDelete.ToolTip = Lang.Text("Common.Action.Delete");
ToolTipService.SetPlacement(btnDelete, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnDelete, 30d);
ToolTipService.SetHorizontalOffset(btnDelete, 2d);
btnDelete.Click += (sender, e) => Delete_Click((MyIconButton)sender, e);
if (sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
{
var btnDisable = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/circle-minus", Tag = sender };
btnDisable.ToolTip = Lang.Text("Instance.Resource.Disable");
ToolTipService.SetPlacement(btnDisable, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnDisable, 30d);
ToolTipService.SetHorizontalOffset(btnDisable, 2d);
btnDisable.Click += (ss, e) => Disable_Click((MyIconButton)ss, e);
sender.Buttons = new[] { btnCont, btnOpen, btnDisable, btnDelete };
}
else if (sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled)
{
var btnEnable = new MyIconButton { LogoScale = 1d, SvgIcon = "lucide/circle-check", Tag = sender };
btnEnable.ToolTip = Lang.Text("Instance.Resource.Enable");
ToolTipService.SetPlacement(btnEnable, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnEnable, 30d);
ToolTipService.SetHorizontalOffset(btnEnable, 2d);
btnEnable.Click += (ss, e) => Enable_Click((MyIconButton)ss, e);
sender.Buttons = new[] { btnCont, btnOpen, btnEnable, btnDelete };
}
else
{
sender.Buttons = new[] { btnCont, btnOpen, btnDelete };
}
}
/// <summary>
/// 刷新整个 UI。
/// </summary>
public void RefreshUI()
{
if (PanList is null)
return;
var showingDatapacks = (IsSearching ? searchResult : datapackItems.Values.Select(i => i.Entry))
.Where(m => CanPassFilter(m)).ToList();
// 对显示的数据包进行排序
if (showingDatapacks.Any())
{
var sortMethod = GetSortMethod(currentSortMethod);
showingDatapacks.Sort((a, b) => sortMethod(a, b));
}
// 重新列出列表
ModAnimation.AniControlEnabled += 1;
if (showingDatapacks.Any())
{
PanList.Visibility = Visibility.Visible;
PanList.Children.Clear();
foreach (var TargetDatapack in showingDatapacks)
{
if (!datapackItems.ContainsKey(TargetDatapack.RawPath))
continue;
var item = datapackItems[TargetDatapack.RawPath];
// 确保元素没有父容器,避免重复添加异常
if (item.Parent is not null) ((Panel)item.Parent).Children.Remove(item);
ModStyle.MinecraftFormatter.SetColorfulTextLab(item.LabTitle.Text, item.LabTitle,
ThemeService.IsDarkMode);
ModStyle.MinecraftFormatter.SetColorfulTextLab(item.LabInfo.Text, item.LabInfo,
ThemeService.IsDarkMode);
item.Checked = selectedDatapacks.Contains(TargetDatapack.RawPath); // 更新选中状态
PanList.Children.Add(item);
}
}
else
{
PanList.Visibility = Visibility.Collapsed;
}
ModAnimation.AniControlEnabled -= 1;
selectedDatapacks =
new HashSet<string>(selectedDatapacks.Where(m =>
showingDatapacks.Any(s => (s.RawPath ?? "") == (m ?? ""))));
RefreshBars();
}
/// <summary>
/// 刷新顶栏和底栏显示。
/// </summary>
public void RefreshBars()
{
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
// -----------------
// 顶部栏
// -----------------
// 计数
var anyCount = 0;
var enabledCount = 0;
var disabledCount = 0;
var updateCount = 0;
var unavalialeCount = 0;
var itemSource = (IsSearching ? searchResult : datapackItems.Values.Select(i => i.Entry)).ToArray();
await Task.Run(() =>
{
foreach (var item in itemSource)
{
anyCount += 1;
if (item.CanUpdate) updateCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) enabledCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) disabledCount += 1;
if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) unavalialeCount += 1;
}
});
// 显示
BtnFilterAll.Text = IsSearching ? Lang.Text("Instance.Resource.Filter.SearchResult") : Lang.Text("Instance.Resource.Filter.AllWithCount", anyCount);
BtnFilterCanUpdate.Text = Lang.Text("Instance.Resource.Filter.UpdatableWithCount", updateCount);
BtnFilterCanUpdate.Visibility = Filter == FilterType.CanUpdate || updateCount > 0
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterEnabled.Text = Lang.Text("Instance.Resource.Filter.EnabledWithCount", enabledCount);
BtnFilterEnabled.Visibility = Filter == FilterType.Enabled || (enabledCount > 0 && enabledCount < anyCount)
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterDisabled.Text = Lang.Text("Instance.Resource.Filter.DisabledWithCount", disabledCount);
BtnFilterDisabled.Visibility = Filter == FilterType.Disabled || disabledCount > 0
? Visibility.Visible
: Visibility.Collapsed;
BtnFilterError.Text = Lang.Text("Instance.Resource.Filter.ErrorWithCount", unavalialeCount);
BtnFilterError.Visibility = Filter == FilterType.Unavailable || unavalialeCount > 0
? Visibility.Visible
: Visibility.Collapsed;
// -----------------
// 底部栏
// -----------------
// 计数
var newCount = selectedDatapacks.Count;
var selected = newCount > 0;
if (selected)
LabSelect.Text = Lang.Text("Instance.Resource.SelectedCount", newCount);
// 按钮可用性
if (selected)
{
var hasUpdate = false;
var hasEnabled = false;
var hasDisabled = false;
var canFavoriteAndShare = true;
// 检查是否所有选中的数据包都有有效的项目信息
await Task.Run(() =>
{
foreach (var DatapackEntity in ModLocalComp.compResourceListLoader.output)
if (selectedDatapacks.Contains(DatapackEntity.RawPath))
{
if (DatapackEntity.CanUpdate) hasUpdate = true;
if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
hasEnabled = true;
else if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled)
hasDisabled = true;
if (DatapackEntity.Comp is null || string.IsNullOrEmpty(DatapackEntity.Comp.Id))
canFavoriteAndShare = false;
}
});
BtnSelectDisable.IsEnabled = hasEnabled;
BtnSelectEnable.IsEnabled = hasDisabled;
BtnSelectUpdate.IsEnabled = hasUpdate;
BtnSelectFavorites.IsEnabled = canFavoriteAndShare;
BtnSelectShare.IsEnabled = canFavoriteAndShare;
}
// 更新显示状态
if (ModAnimation.AniControlEnabled == 0)
{
PanListBack.Margin = new Thickness(0d, 0d, 0d, selected ? 95 : 15);
if (selected)
{
// 仅在数量增加时播放出现/跳跃动画
if (bottomBarShownCount >= newCount)
{
bottomBarShownCount = newCount;
return;
}
bottomBarShownCount = newCount;
// 出现/跳跃动画
CardSelect.Visibility = Visibility.Visible;
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(CardSelect, 1d - CardSelect.Opacity, 60),
ModAnimation.AaTranslateY(CardSelect, -27 - TransSelect.Y, 120,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaTranslateY(CardSelect, 3d, 150, 120,
new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaTranslateY(CardSelect, -1, 90, 270,
new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak))
}, "Datapack Sidebar");
}
else
{
// 不重复播放隐藏动画
if (bottomBarShownCount == 0)
return;
bottomBarShownCount = 0;
// 隐藏动画
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(CardSelect, -CardSelect.Opacity, 90),
ModAnimation.AaTranslateY(CardSelect, -10 - TransSelect.Y, 90,
ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaCode(() => CardSelect.Visibility = Visibility.Collapsed, after: true)
}, "Datapack Sidebar");
}
}
else
{
ModAnimation.AniStop("Datapack Sidebar");
bottomBarShownCount = newCount;
if (selected)
{
CardSelect.Visibility = Visibility.Visible;
CardSelect.Opacity = 1d;
TransSelect.Y = -25;
}
else
{
CardSelect.Visibility = Visibility.Collapsed;
CardSelect.Opacity = 0d;
TransSelect.Y = -10;
}
}
}));
}
private int bottomBarShownCount;
#endregion
#region
/// <summary>
/// 打开 datapacks 文件夹。
/// </summary>
private void BtnManageOpen_Click(object sender, EventArgs e)
{
try
{
var datapackPath = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
Directory.CreateDirectory(datapackPath);
ModBase.OpenExplorer(datapackPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"打开 datapacks 文件夹失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
/// <summary>
/// 全选。
/// </summary>
private void BtnManageSelectAll_Click(object sender, MouseButtonEventArgs e)
{
ChangeAllSelected(selectedDatapacks.Count < PanList.Children.Count);
}
/// <summary>
/// 安装数据包。
/// </summary>
private void BtnManageInstall_Click(object sender, MouseButtonEventArgs e)
{
var fileList = SystemDialogs.SelectFiles(
Lang.Text("Instance.Saves.Datapack.Install.FileDialog.Filter"),
Lang.Text("Instance.Saves.Datapack.Install.FileDialog.Title"));
if (fileList is null || fileList.Length == 0)
return;
InstallDatapackFiles(fileList);
Refresh();
}
/// <summary>
/// 安装数据包文件。
/// </summary>
public static void InstallDatapackFiles(IEnumerable<string> filePathList)
{
if (!filePathList.Any())
return;
var extension = filePathList.First().AfterLast(".").ToLower();
// 检查文件扩展名
if (extension != "zip")
{
HintService.Hint(Lang.Text("Instance.Resource.Install.UnsupportedFormat", extension, Lang.Text("Download.Comp.Type.DataPack"), "zip"), HintType.Error);
return;
}
// 检查回收站
if (filePathList.First().Contains(@":\$RECYCLE.BIN\"))
{
HintService.Hint(Lang.Text("Instance.Resource.Install.RestoreFromRecycleBin"), HintType.Error);
return;
}
ModBase.Log($"[System] 文件为 {extension} 格式,尝试作为数据包安装");
// 确认安装
if (!(ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup &&
ModMain.frmMain.PageCurrentSub == FormMain.PageSubType.VersionSavesDatapack))
if (ModMain.MyMsgBox(Lang.Text("Instance.Saves.Datapack.Install.Message"),
Lang.Text("Instance.Saves.Datapack.Install.Title"), Lang.Text("Common.Action.Confirm"),
Lang.Text("Common.Action.Cancel")) != 1)
return;
// 执行安装
try
{
var datapackFolder = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
Directory.CreateDirectory(datapackFolder);
foreach (var FilePath in filePathList)
{
var newFileName = ModBase.GetFileNameFromPath(FilePath);
var destFile = datapackFolder + newFileName;
if (File.Exists(destFile))
if (ModMain.MyMsgBox(Lang.Text("Instance.Resource.Install.OverwriteConfirm.Message", newFileName), Lang.Text("Instance.Resource.Install.OverwriteConfirm.Title"), Lang.Text("Common.Action.Overwrite"), Lang.Text("Common.Action.Cancel")) != 1)
continue;
ModBase.CopyFile(FilePath, destFile);
}
if (filePathList.Count() == 1)
HintService.Hint(Lang.Text("Instance.Resource.Install.SuccessSingle", ModBase.GetFileNameFromPath(filePathList.First())), HintType.Success);
else
HintService.Hint(Lang.Text("Instance.Resource.Install.SuccessMultiple", filePathList.Count(), Lang.Text("Download.Comp.Type.DataPack")), HintType.Success);
// 刷新列表
if (ModMain.frmMain.pageCurrent == FormMain.PageType.InstanceSetup &&
ModMain.frmMain.PageCurrentSub == FormMain.PageSubType.VersionSavesDatapack)
if (ModMain.frmInstanceSavesDatapack is not null)
ModMain.frmInstanceSavesDatapack.ReloadDatapackFileList(true);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"复制数据包文件失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
/// <summary>
/// 下载数据包。
/// </summary>
private void BtnManageDownload_Click(object sender, MouseButtonEventArgs e)
{
var datapackPath = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
Directory.CreateDirectory(datapackPath);
PageDownloadCompDetail.cachedFolder[ModComp.CompType.DataPack] = datapackPath;
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadDataPack);
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
}
/// <summary>
/// 导出信息。
/// </summary>
private void BtnManageInfoExport_Click(object sender, MouseButtonEventArgs e)
{
var choice =
ModMain.MyMsgBox(
Lang.Text("Instance.Saves.Datapack.Export.Mode.Message"),
Lang.Text("Instance.Resource.Export.Mode.Title"), Lang.Text("Instance.Resource.Export.Mode.Txt"), Lang.Text("Instance.Resource.Export.Mode.Csv"), Lang.Text("Common.Action.Cancel"));
void ExportText(string content, string fileName)
{
try
{
var savePath =
SystemDialogs.SelectSaveFile(Lang.Text("Instance.Resource.Export.SelectSaveLocation"), fileName, Lang.Text("Instance.Resource.Export.FilesFilter"));
if (string.IsNullOrWhiteSpace(savePath)) return;
File.WriteAllText(savePath, content, Encoding.UTF8);
ModBase.OpenExplorer(savePath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"导出数据包信息失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
;
switch (choice)
{
case 1: // TXT
{
var exportContent = new List<string>();
foreach (var DatapackEntity in ModLocalComp.compResourceListLoader.output)
exportContent.Add(DatapackEntity.FileName);
ExportText(exportContent.Join("\r\n"),
ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.currentSave) + "的数据包信息.txt");
break;
}
case 2: // CSV
{
var exportContent = new List<string>();
exportContent.Add("文件名,数据包名称,数据包版本,此版本更新时间,工程 ID,文件大小(字节),文件路径");
foreach (var DatapackEntity in ModLocalComp.compResourceListLoader.output)
exportContent.Add(
$"{DatapackEntity.FileName},{DatapackEntity.Comp?.TranslatedName},{DatapackEntity.Version},{DatapackEntity.compFile?.ReleaseDate},{DatapackEntity.Comp?.Id},{GetDatapackFileInfo(DatapackEntity.path).Length},{DatapackEntity.path}");
ExportText(exportContent.Join("\r\n"),
ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.currentSave) + "的数据包信息.csv");
break;
}
}
}
#endregion
#region
/// <summary>
/// 选择的数据包的路径。
/// </summary>
public HashSet<string> selectedDatapacks = new();
// 单项切换选择状态
public void CheckChanged(MyLocalCompItem sender, ModBase.RouteEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
// 更新选择了的内容
var selectedKey = sender.Entry.RawPath;
if (sender.Checked)
selectedDatapacks.Add(selectedKey);
else
selectedDatapacks.Remove(selectedKey);
RefreshBars();
}
// 切换所有项的选择状态
private void ChangeAllSelected(bool value)
{
ModAnimation.AniControlEnabled += 1;
selectedDatapacks.Clear();
foreach (var Item in datapackItems.Values)
{
var shouldSelected = value && PanList.Children.Contains(Item);
Item.Checked = shouldSelected;
if (shouldSelected)
selectedDatapacks.Add(Item.Entry.RawPath);
}
ModAnimation.AniControlEnabled -= 1;
RefreshBars();
}
private void UnselectedAllWithAnimation()
{
var cacheAniControlEnabled = ModAnimation.AniControlEnabled;
ModAnimation.AniControlEnabled = 0;
ChangeAllSelected(false);
ModAnimation.AniControlEnabled += cacheAniControlEnabled;
}
private void FrmMain_KeyDown(object sender, KeyEventArgs e)
{
if (!ReferenceEquals(ModMain.frmMain.pageRight, this))
return;
if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A)
ChangeAllSelected(true);
}
private void SearchBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
// Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获
if (SearchBox.Text.Any())
return;
if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A)
PanBack.Focus();
}
#endregion
#region
public FilterType Filter
{
get => field;
set
{
if (field == value)
return;
field = value;
switch (value)
{
case FilterType.All:
{
BtnFilterAll.Checked = true;
break;
}
case FilterType.Enabled:
{
BtnFilterEnabled.Checked = true;
break;
}
case FilterType.Disabled:
{
BtnFilterDisabled.Checked = true;
break;
}
case FilterType.CanUpdate:
{
BtnFilterCanUpdate.Checked = true;
break;
}
default:
{
BtnFilterError.Checked = true;
break;
}
}
RefreshUI();
}
} = FilterType.All;
public enum FilterType
{
All = 0,
Enabled = 1,
Disabled = 2,
CanUpdate = 3,
Unavailable = 4
}
/// <summary>
/// 检查该数据包项是否符合当前筛选的类别。
/// </summary>
private bool CanPassFilter(ModLocalComp.LocalCompFile checkingDatapack)
{
switch (Filter)
{
case FilterType.All:
{
return true;
}
case FilterType.Enabled:
{
return checkingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine;
}
case FilterType.Disabled:
{
return checkingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled;
}
case FilterType.CanUpdate:
{
return checkingDatapack.CanUpdate;
}
case FilterType.Unavailable:
{
return checkingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable;
}
default:
{
return false;
}
}
}
// 点击筛选项触发的改变
private void ChangeFilter(MyRadioButton sender, bool raiseByMouse)
{
Filter = (FilterType)Convert.ToInt32(sender.Tag);
RefreshUI();
DoSort();
}
#endregion
#region
private SortMethod currentSortMethod = SortMethod.CompName;
private void SetSortMethod(SortMethod target)
{
currentSortMethod = target;
BtnSort.Text = Lang.Text("Instance.Resource.Sort.Text", GetSortName(target));
DoSort();
}
private enum SortMethod
{
FileName,
CompName,
CreateTime,
DatapackFileSize
}
private string GetSortName(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return Lang.Text("Instance.Resource.Sort.FileName");
}
case SortMethod.CompName:
{
return Lang.Text("Instance.Resource.Sort.ResourceName");
}
case SortMethod.CreateTime:
{
return Lang.Text("Instance.Resource.Sort.AddTime");
}
case SortMethod.DatapackFileSize:
{
return Lang.Text("Instance.Resource.Sort.FileSize");
}
default:
{
return Lang.Text("Instance.Resource.Sort.ResourceName");
}
}
return "";
}
private void BtnSortClick(object sender, ModBase.RouteEventArgs e)
{
var body = new ContextMenu();
foreach (SortMethod i in Enum.GetValues(typeof(SortMethod)))
{
var item = new MyMenuItem();
item.Header = GetSortName(i);
item.Click += (_, _) => SetSortMethod(i);
body.Items.Add(item);
}
body.PlacementTarget = (UIElement)sender;
body.Placement = PlacementMode.Bottom;
body.IsOpen = true;
}
private readonly object sortLock = new();
private void DoSort()
{
lock (sortLock)
{
try
{
if (PanList is null || PanList.Children.Count < 2)
return;
// 将子元素转换为可排序的列表
var items = PanList.Children.OfType<MyLocalCompItem>().ToList();
var method = GetSortMethod(currentSortMethod);
// 分离有效和无效项(保持原始相对顺序)
var invalid = items.Where(i => i.Entry is null).ToList();
var valid = items.Except(invalid).ToList();
// 仅对有效项进行排序
valid.Sort((x, y) => method(x.Entry, y.Entry));
// 合并保持无效项的原始顺序
items = valid.Concat(invalid).ToList();
// 批量更新UI元素
PanList.Children.Clear();
items.ForEach(i => PanList.Children.Add(i));
}
catch (Exception ex)
{
ModBase.Log(
ex,
"执行排序时出错",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
}
private Func<ModLocalComp.LocalCompFile, ModLocalComp.LocalCompFile, int> GetSortMethod(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return (a, b) => string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase);
}
case SortMethod.CompName:
{
return (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
}
case SortMethod.CreateTime:
{
return (a, b) =>
{
var aDate = GetDatapackFileInfo(a.path).CreationTime;
var bDate = GetDatapackFileInfo(b.path).CreationTime;
if (aDate == DateTime.MinValue && bDate == DateTime.MinValue)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
if (aDate == DateTime.MinValue) return 1;
if (bDate == DateTime.MinValue) return -1;
return bDate.CompareTo(aDate);
};
}
case SortMethod.DatapackFileSize:
{
return (a, b) =>
{
var aSize = GetDatapackFileInfo(a.path).Length;
var bSize = GetDatapackFileInfo(b.path).Length;
if (aSize == 0L && bSize == 0L)
return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
if (aSize == 0L) return 1;
if (bSize == 0L) return -1;
return bSize.CompareTo(aSize);
};
}
default:
{
return (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
}
}
}
#endregion
#region
// 启用
private void BtnSelectEnable_Click(object sender, ModBase.RouteEventArgs e)
{
ToggleDatapacks(
ModLocalComp.compResourceListLoader.output.Where(m => selectedDatapacks.Contains(m.RawPath)).ToList(),
true);
ChangeAllSelected(false);
}
// 禁用
private void BtnSelectDisable_Click(object sender, ModBase.RouteEventArgs e)
{
ToggleDatapacks(
ModLocalComp.compResourceListLoader.output.Where(m => selectedDatapacks.Contains(m.RawPath)).ToList(),
false);
ChangeAllSelected(false);
}
/// <summary>
/// 启用/禁用数据包(通过重命名文件夹为 .disabled)
/// </summary>
private void ToggleDatapacks(IEnumerable<ModLocalComp.LocalCompFile> datapackList, bool isEnable)
{
var isSuccessful = true;
foreach (var DatapackE in datapackList)
{
var datapackEntity = DatapackE;
string newPath = null;
if (datapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine && !isEnable)
// 禁用 - 添加 .disabled 后缀
newPath = datapackEntity.path + ".disabled";
else if (datapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled && isEnable)
// 启用 - 移除 .disabled 后缀
newPath = datapackEntity.RawPath;
else
continue;
// 重命名
try
{
if (File.Exists(newPath))
{
ModMain.MyMsgBox(Lang.Text("Instance.Saves.Datapack.Replace.FileNameConflict", ModBase.GetFileNameFromPath(newPath)));
continue;
}
FileSystem.Rename(datapackEntity.path, newPath);
}
catch (FileNotFoundException ex)
{
ModBase.Log(
ex,
$"未找到需要重命名的数据包({datapackEntity.path ?? "null"}",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
ReloadDatapackFileList(true);
return;
}
catch (Exception ex)
{
ModBase.Log(ex, $"重命名数据包失败({datapackEntity.path ?? "null"}");
isSuccessful = false;
}
// 更改 Loader 中的列表
var newDatapackEntity = new ModLocalComp.LocalCompFile(newPath);
newDatapackEntity.FromJson(datapackEntity.ToJson());
if (ModLocalComp.compResourceListLoader.output.Contains(datapackEntity))
{
var indexOfLoader = ModLocalComp.compResourceListLoader.output.IndexOf(datapackEntity);
ModLocalComp.compResourceListLoader.output.RemoveAt(indexOfLoader);
ModLocalComp.compResourceListLoader.output.Insert(indexOfLoader, newDatapackEntity);
}
if (searchResult is not null && searchResult.Contains(datapackEntity))
{
var indexOfResult = searchResult.IndexOf(datapackEntity);
searchResult.Remove(datapackEntity);
searchResult.Insert(indexOfResult, newDatapackEntity);
}
// 更改 UI 中的列表
try
{
var newItem = BuildLocalCompItem(newDatapackEntity);
datapackItems[datapackEntity.RawPath] = newItem;
var indexOfUi = PanList.Children.IndexOf(PanList.Children.OfType<MyLocalCompItem>()
.FirstOrDefault(i => ReferenceEquals(i.Entry, datapackEntity)));
if (indexOfUi == -1)
continue;
PanList.Children.RemoveAt(indexOfUi);
PanList.Children.Insert(indexOfUi, newItem);
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"更新 UI 列表项失败:{datapackEntity.FileName}",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
Dispatcher.Invoke(() => PanList.UpdateLayout(), DispatcherPriority.Background);
if (isSuccessful)
{
RefreshBars();
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.Datapack.ToggleWarning"), HintType.Error);
ReloadDatapackFileList(true);
}
LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly);
}
// 更新
private void BtnSelectUpdate_Click(object sender, ModBase.RouteEventArgs e)
{
var updateList = ModLocalComp.compResourceListLoader.output
.Where(m => selectedDatapacks.Contains(m.RawPath) && m.CanUpdate).ToList();
if (!updateList.Any())
return;
UpdateResource(updateList);
ChangeAllSelected(false);
}
/// <summary>
/// 记录正在进行数据包更新的 datapacks 文件夹路径。
/// </summary>
public static List<string> updatingVersions = new();
private static bool TryGetSafeDatapackUpdateFileName(ModComp.CompFile file, out string fileName)
{
fileName = file.FileName?.Trim() ?? "";
if (string.IsNullOrEmpty(fileName))
return false;
if (!fileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
return false;
if (fileName.IndexOfAny(new[] { '\\', '/', ':' }) >= 0)
return false;
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
return false;
return fileName == Path.GetFileName(fileName) && fileName != "." && fileName != "..";
}
private static bool TryBuildDatapackUpdatePath(string rootPath, string fileName, out string fullPath)
{
var fullRootPath = Path.GetFullPath(rootPath);
if (!fullRootPath.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
!fullRootPath.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
fullRootPath += Path.DirectorySeparatorChar;
fullPath = Path.GetFullPath(Path.Combine(fullRootPath, fileName));
return fullPath.StartsWith(fullRootPath, StringComparison.OrdinalIgnoreCase);
}
public void UpdateResource(IEnumerable<ModLocalComp.LocalCompFile> datapackList)
{
// 更新前警告
if (!States.Hint.FunctionDatapackUpdate || datapackList.Count() >= 15)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Saves.Datapack.Update.Warning.Message"),
Lang.Text("Instance.Saves.Datapack.Update.Warning.Title"), Lang.Text("Instance.Saves.Datapack.Update.Warning.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 1)
States.Hint.FunctionDatapackUpdate = true;
else
return;
}
try
{
// 构造下载信息
datapackList = datapackList.ToList(); // 防止刷新影响迭代器
var fileList = new List<DownloadFile>();
var fileCopyList = new Dictionary<string, string>();
var updateEntryList = new List<ModLocalComp.LocalCompFile>();
var tempRoot = Path.Combine(ModBase.pathTemp, "DownloadedComp");
var datapackRoot = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
var skippedUnsafeFileCount = 0;
foreach (var Entry in datapackList)
{
var file = Entry.UpdateFile;
if (!file.Available)
continue;
if (!TryGetSafeDatapackUpdateFileName(file, out var safeFileName) ||
!TryBuildDatapackUpdatePath(tempRoot, safeFileName, out var tempAddress) ||
!TryBuildDatapackUpdatePath(datapackRoot, safeFileName, out var realAddress))
{
skippedUnsafeFileCount++;
ModBase.Log($"[DatapackUpdate] 已跳过不安全的数据包更新文件名:{file.FileName}", ModBase.LogLevel.Debug);
continue;
}
// 添加到下载列表
fileList.Add(file.ToNetFile(tempAddress, ModComp.DownloadReason.Update,
file.RawGameVersions.FirstOrDefault()));
fileCopyList[tempAddress] = realAddress;
updateEntryList.Add(Entry);
}
if (skippedUnsafeFileCount > 0)
HintService.Hint(
Lang.Text("Instance.Saves.Datapack.Update.UnsafeFilesSkipped", skippedUnsafeFileCount),
HintType.Error);
if (!fileList.Any())
return;
// 构造加载器
var installLoaders = new List<ModLoader.LoaderBase>();
var finishedFileNames = new List<string>();
installLoaders.Add(
new LoaderDownload(Lang.Text("Instance.Saves.Datapack.Update.Task.DownloadFiles"), fileList)
{ ProgressWeight = updateEntryList.Count * 1.5d });
installLoaders.Add(new ModLoader.LoaderTask<int, int>(
Lang.Text("Instance.Saves.Datapack.Update.Task.ReplaceFiles"), _ =>
{
try
{
foreach (var Entry in updateEntryList)
if (File.Exists(Entry.path))
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.path, UIOption.AllDialogs,
RecycleOption.SendToRecycleBin);
else
ModBase.Log($"[DatapackUpdate] 未找到更新前的数据包文件,跳过对它的删除:{Entry.path}",
ModBase.LogLevel.Debug);
foreach (var Entry in fileCopyList)
{
if (File.Exists(Entry.Value))
{
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Value, UIOption.AllDialogs,
RecycleOption.SendToRecycleBin);
ModBase.Log($"[Datapack] 更新后的数据包文件已存在,将会把它放入回收站:{Entry.Value}", ModBase.LogLevel.Debug);
}
if (Directory.Exists(ModBase.GetPathFromFullPath(Entry.Value)))
{
File.Move(Entry.Key, Entry.Value);
finishedFileNames.Add(ModBase.GetFileNameFromPath(Entry.Value));
}
else
{
ModBase.Log($"[Datapack] 更新后的目标文件夹已被删除:{Entry.Value}", ModBase.LogLevel.Debug);
}
}
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "替换旧版数据包文件时被主动取消");
}
}));
// 结束处理
var loader = new ModLoader.LoaderCombo<IEnumerable<ModLocalComp.LocalCompFile>>(
Lang.Text("Instance.Saves.Datapack.Update.Task.Title",
ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.currentSave)), installLoaders);
var pathDatapacks = Path.Combine(PageInstanceSavesLeft.currentSave, "datapacks");
loader.OnStateChanged = _ =>
{
switch (loader.State)
{
case ModBase.LoadState.Finished:
{
switch (finishedFileNames.Count)
{
case 0:
{
ModBase.Log("[DatapackUpdate] 没有数据包被成功更新");
break;
}
case 1:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.SuccessSingle", finishedFileNames.Single()), HintType.Success);
break;
}
default:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.SuccessMultiple", finishedFileNames.Count), HintType.Success);
break;
}
}
break;
}
case ModBase.LoadState.Failed:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.Failed", loader.Error.Message), HintType.Error);
break;
}
case ModBase.LoadState.Aborted:
{
HintService.Hint(Lang.Text("Instance.Resource.Update.Aborted"));
break;
}
default:
{
return;
}
}
ModBase.Log($"[DatapackUpdate] 已从正在进行数据包更新的文件夹列表移除:{pathDatapacks}");
updatingVersions.Remove(pathDatapacks);
// 清理缓存
ModBase.RunInNewThread(() =>
{
try
{
foreach (var TempFile in fileCopyList.Keys)
if (File.Exists(TempFile))
File.Delete(TempFile);
}
catch (Exception ex)
{
ModBase.Log(ex, "清理数据包更新缓存失败");
}
}, "Clean Datapack Update Cache", ThreadPriority.BelowNormal);
};
// 启动加载器
ModBase.Log($"[DatapackUpdate] 开始更新 {datapackList.Count()} 个数据包:{pathDatapacks}");
updatingVersions.Add(pathDatapacks);
loader.Start();
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
ReloadDatapackFileList(true);
}
catch (Exception ex)
{
ModBase.Log(ex, "初始化数据包更新失败");
}
}
// 删除
private void BtnSelectDelete_Click(object sender, ModBase.RouteEventArgs e)
{
DeleteDatapacks(ModLocalComp.compResourceListLoader.output.Where(m => selectedDatapacks.Contains(m.RawPath)));
ChangeAllSelected(false);
}
private void DeleteDatapacks(IEnumerable<ModLocalComp.LocalCompFile> datapackList)
{
try
{
var isSuccessful = true;
var isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
// 确认需要删除的文件
datapackList = datapackList.SelectMany(target =>
{
if (target.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
return new[] { target.path, target.path + ".disabled" };
return new[] { target.path, target.RawPath };
}).Distinct().Where(m => File.Exists(m)).Select(m => new ModLocalComp.LocalCompFile(m)).ToList();
// 实际删除文件
foreach (var DatapackEntity in datapackList)
{
try
{
if (isShiftPressed)
File.Delete(DatapackEntity.path);
else
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(DatapackEntity.path,
UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "删除数据包被主动取消");
ReloadDatapackFileList(true);
return;
}
catch (Exception ex)
{
ModBase.Log(
ex,
$"删除数据包失败({DatapackEntity.path}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
isSuccessful = false;
}
// 取消选中
selectedDatapacks.Remove(DatapackEntity.RawPath);
// 更改 Loader 和 UI 中的列表
ModLocalComp.compResourceListLoader.output.Remove(DatapackEntity);
searchResult?.Remove(DatapackEntity);
datapackItems.Remove(DatapackEntity.RawPath);
var indexOfUi = PanList.Children.IndexOf(PanList.Children.OfType<MyLocalCompItem>()
.FirstOrDefault(i => i.Entry.Equals(DatapackEntity)));
if (indexOfUi >= 0)
PanList.Children.RemoveAt(indexOfUi);
}
RefreshBars();
if (!isSuccessful)
{
HintService.Hint(Lang.Text("Instance.Saves.Datapack.Delete.FileOccupied"), HintType.Error);
ReloadDatapackFileList(true);
}
else if (PanList.Children.Count == 0)
{
ReloadDatapackFileList(true);
}
else
{
RefreshBars();
}
if (!isSuccessful)
return;
if (isShiftPressed)
{
if (datapackList.Count() == 1)
HintService.Hint(Lang.Text("Instance.Saves.Datapack.Delete.PermanentSingle", datapackList.Single().FileName), HintType.Success);
else
HintService.Hint(Lang.Text("Instance.Saves.Datapack.Delete.PermanentMultiple", datapackList.Count()), HintType.Success);
}
else if (datapackList.Count() == 1)
{
HintService.Hint(Lang.Text("Instance.Saves.Datapack.Delete.RecycleSingle", datapackList.Single().FileName), HintType.Success);
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.Datapack.Delete.RecycleMultiple", datapackList.Count()), HintType.Success);
}
}
catch (OperationCanceledException ex)
{
ModBase.Log(ex, "删除数据包被主动取消");
ReloadDatapackFileList(true);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"删除数据包出现未知错误",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
ReloadDatapackFileList(true);
}
LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly);
}
// 取消选择
private void BtnSelectCancel_Click(object sender, ModBase.RouteEventArgs e)
{
ChangeAllSelected(false);
}
// 收藏
private void BtnSelectFavorites_Click(object sender, ModBase.RouteEventArgs e)
{
var selected = ModLocalComp.compResourceListLoader.output
.Where(m => selectedDatapacks.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp).ToList();
ModComp.CompFavorites.ShowMenu(selected, (UIElement)sender);
}
// 分享
private void BtnSelectShare_Click(object sender, ModBase.RouteEventArgs e)
{
var shareList = ModLocalComp.compResourceListLoader.output
.Where(m => selectedDatapacks.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp.Id).ToHashSet();
ModBase.ClipboardSet(ModComp.CompFavorites.GetShareCode(shareList));
ChangeAllSelected(false);
}
#endregion
#region
// 详情
public void Info_Click(object sender, EventArgs e)
{
try
{
var datapackEntry = ((MyLocalCompItem)(sender is MyIconButton iconBtn ? iconBtn.Tag : sender)).Entry;
// 加载失败信息
if (datapackEntry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable)
{
ModMain.MyMsgBox(
Lang.Text(
"Instance.Saves.Datapack.Info.ReadFailed.WithDetail",
datapackEntry.FileUnavailableReason.ToString()),
Lang.Text("Instance.Saves.Datapack.Info.ReadFailedTitle"));
return;
}
if (datapackEntry.Comp is not null)
{
// 跳转到数据包下载页面
ModMain.frmMain.PageChange(new FormMain.PageStackData
{
page = FormMain.PageType.CompDetail,
additional = (datapackEntry.Comp, new List<string>(), PageInstanceLeft.McInstance.Info.VanillaName,
ModComp.CompLoaderType.Minecraft, ModComp.CompType.DataPack, null)
});
}
else
{
// 获取信息
var contentLines = new List<string>();
if (datapackEntry.Description is not null)
contentLines.Add(datapackEntry.Description + "\r\n");
if (datapackEntry.Authors is not null)
contentLines.Add(Lang.Text("Instance.Saves.Datapack.Info.Author") + datapackEntry.Authors);
contentLines.Add(Lang.Text("Instance.Saves.Datapack.Info.File") + datapackEntry.FileName + "" +
ModBase.GetString(GetDatapackFileInfo(datapackEntry.path).Length) + "");
if (datapackEntry.Version is not null)
contentLines.Add(Lang.Text("Instance.Saves.Datapack.Info.Version") + datapackEntry.Version);
var debugInfo = new List<string>();
if (datapackEntry.ModId is not null) debugInfo.Add(Lang.Text("Instance.Saves.Datapack.Info.DatapackId") + datapackEntry.ModId);
if (debugInfo.Any())
{
contentLines.Add("");
contentLines.AddRange(debugInfo);
}
// 显示详情信息
if (datapackEntry.Url is null)
ModMain.MyMsgBox(contentLines.Join("\r\n"), datapackEntry.Name, Lang.Text("Instance.Resource.Item.Info.Return"));
else if (ModMain.MyMsgBox(contentLines.Join("\r\n"), datapackEntry.Name, Lang.Text("Instance.Resource.Item.Info.OpenWebsite"), Lang.Text("Instance.Resource.Item.Info.Return")) == 1)
ModBase.OpenWebsite(datapackEntry.Url);
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"获取数据包详情失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
// 打开文件所在的位置
public void Open_Click(MyIconButton sender, EventArgs e)
{
try
{
var listItem = (MyLocalCompItem)sender.Tag;
ModBase.OpenExplorer(listItem.Entry.path);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"打开数据包文件位置失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
// 删除
public void Delete_Click(MyIconButton sender, EventArgs e)
{
var listItem = (MyLocalCompItem)sender.Tag;
DeleteDatapacks(new[] { listItem.Entry });
}
// 启用
public void Enable_Click(MyIconButton sender, EventArgs e)
{
var listItem = (MyLocalCompItem)sender.Tag;
ToggleDatapacks(new[] { listItem.Entry }, true);
}
// 禁用
public void Disable_Click(MyIconButton sender, EventArgs e)
{
var listItem = (MyLocalCompItem)sender.Tag;
ToggleDatapacks(new[] { listItem.Entry }, false);
}
#endregion
#region
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text);
private List<ModLocalComp.LocalCompFile> searchResult;
public void SearchRun(object sender, EventArgs e)
{
try
{
if (IsSearching)
{
// 构造请求
var queryList = new List<ModBase.SearchEntry<ModLocalComp.LocalCompFile>>();
foreach (var Entry in ModLocalComp.compResourceListLoader.output)
{
var searchSource = new List<ModBase.SearchSource>();
searchSource.Add(new ModBase.SearchSource(Entry.Name, 1d));
searchSource.Add(new ModBase.SearchSource(Entry.FileName, 1d));
if (Entry.Version is not null)
searchSource.Add(new ModBase.SearchSource(Entry.Version, 0.2d));
if (Entry.Description is not null && !string.IsNullOrEmpty(Entry.Description))
searchSource.Add(new ModBase.SearchSource(Entry.Description, 0.4d));
if (Entry.Comp is not null)
{
if ((Entry.Comp.RawName ?? "") != (Entry.Name ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.RawName, 1d));
if ((Entry.Comp.TranslatedName ?? "") != (Entry.Comp.RawName ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.TranslatedName, 1d));
if ((Entry.Comp.Description ?? "") != (Entry.Description ?? ""))
searchSource.Add(new ModBase.SearchSource(Entry.Comp.Description, 0.4d));
searchSource.Add(new ModBase.SearchSource(string.Join("", Entry.Comp.Tags), 0.2d));
}
queryList.Add(new ModBase.SearchEntry<ModLocalComp.LocalCompFile>
{ item = Entry, searchSource = searchSource });
}
// 进行搜索
searchResult = ModBase.Search(queryList, SearchBox.Text, ModBase.MaxLocalSearchDepth, 0.35d).Select(r => r.item).ToList();
}
RefreshUI();
}
catch (Exception ex)
{
ModBase.Log(ex, "搜索过程中发生异常");
}
}
#endregion
}
@@ -0,0 +1,33 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceSavesInfo"
PanScroll="{Binding ElementName=PanBack}">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<StackPanel Margin="25,10,25,10" x:Name="PanMain">
<local:MyHint x:Name="Hintversion1_9" Theme="Yellow" Visibility="Collapsed" Margin="0,5,0,0" />
<local:MyHint x:Name="Hintversion1_8" Theme="Yellow" Visibility="Collapsed" Margin="0,5,0,0" />
<local:MyHint x:Name="Hintversion1_3" Theme="Yellow" Visibility="Collapsed" Margin="0,5,0,0" />
<local:MyCard Margin="0,15,0,0" Title="{DynamicResource Instance.Saves.Info.Details.Title}" x:Name="PanContent">
<Grid Margin="15,35,15,15" x:Name="PanList">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="TextBlock" Width="Auto" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</local:MyCard>
<local:MyCard Margin="0,15,0,0" Title="{DynamicResource Instance.Saves.Info.Settings.Title}" x:Name="PanSettings" Visibility="Collapsed">
<Grid Margin="15,35,15,15" x:Name="PanSettingsList">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="TextBlock" Width="Auto" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,356 @@
using System.Windows;
using System.Windows.Controls;
using Humanizer;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.Minecraft.Saves;
using PCL.Core.Minecraft.Saves.Editing;
using PCL.Core.UI;
namespace PCL;
public partial class PageInstanceSavesInfo : IRefreshable
{
/// <summary>无状态服务,线程安全,所有实例可共享。</summary>
private static readonly SaveManager SaveManager = new();
/// <summary>防并发冲突</summary>
private static readonly SemaphoreSlim WriteLock = new(1, 1);
private CancellationTokenSource? _cts;
public PageInstanceSavesInfo()
{
InitializeComponent();
Loaded += async (_, _) =>
{
PanBack.ScrollToHome();
await RefreshInfoAsync();
};
}
void IRefreshable.Refresh() => Refresh();
public void Refresh() => RefreshInfoAsync().ContinueWith(
t => LogWrapper.Warn(t.Exception, "Saves", "刷新存档信息异常"), //only 兜底
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
private async Task RefreshInfoAsync()
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
var ct = _cts.Token;
try
{
ClearInfoTable();
PanSettingsList.Children.Clear();
PanSettingsList.RowDefinitions.Clear();
Hintversion1_9.Visibility = Visibility.Collapsed;
Hintversion1_8.Visibility = Visibility.Collapsed;
Hintversion1_3.Visibility = Visibility.Collapsed;
PanSettings.Visibility = Visibility.Collapsed;
var save = await SaveManager.LoadSaveAsync(PageInstanceSavesLeft.currentSave, ct);
ModMain.frmInstanceSavesLeft.ItemDatapack.Visibility =
save.VersionId is null or < DataVersionBoundaries._17w47a ? Visibility.Collapsed : Visibility.Visible;
if (save.VersionName is null)
{
if (save.Difficulty.HasValue)
ShowHint(Hintversion1_9, "Instance.Saves.Info.VersionHint.1_9");
else if (save.AllowCommands)
ShowHint(Hintversion1_8, "Instance.Saves.Info.VersionHint.1_8");
else
ShowHint(Hintversion1_3, "Instance.Saves.Info.VersionHint.1_3");
}
else
AddInfoRow(Lang.Text("Instance.Saves.Info.Version"), $"{save.VersionName} ({save.VersionId})");
AddInfoRow(Lang.Text("Instance.Saves.Info.LevelName"), save.LevelName);
AddInfoRow(Lang.Text("Instance.Saves.Info.Seed"),
save.Seed?.ToString() ?? Lang.Text("Instance.Saves.Info.GetFailed"),
isSeed: true, versionName: save.VersionName);
AddInfoRow(Lang.Text("Instance.Saves.Info.LastPlayed"),
Lang.Date(save.LastPlayedUtc.ToLocalTime(), "g"));
if (save.Spawn.HasValue)
{
var s = save.Spawn.Value;
AddInfoRow(Lang.Text("Instance.Saves.Info.SpawnPoint"), $"{s.X:F0} / {s.Y:F0} / {s.Z:F0}");
}
AddInfoRow(Lang.Text("Instance.Saves.Info.GameMode"), GameModeName(save.GameMode));
AddInfoRow(Lang.Text("Instance.Saves.Info.PlayTime"),
Lang.TimeSpan(save.PlayTime, 3, false, TimeUnit.Day, TimeUnit.Second));
if (save.VersionName is not null || save.Difficulty.HasValue)
BuildAllowCommandsSetting(save.AllowCommands);
if (save.Difficulty.HasValue)
BuildDifficultySetting(save.IsHardcore, save.IsDifficultyLocked, (int)save.Difficulty.Value);
PanContent.Visibility = Visibility.Visible;
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Info.Error.LoadFailed"),
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Saves.Info.Error.LoadFailed"));
PanContent.Visibility = Visibility.Collapsed;
PanSettings.Visibility = Visibility.Collapsed;
PanSettingsList.Children.Clear();
PanSettingsList.RowDefinitions.Clear();
Hintversion1_9.Visibility = Visibility.Collapsed;
Hintversion1_8.Visibility = Visibility.Collapsed;
Hintversion1_3.Visibility = Visibility.Collapsed;
}
}
private void BuildAllowCommandsSetting(bool allowCommands)
{
PanSettings.Visibility = Visibility.Visible;
var folder = PageInstanceSavesLeft.currentSave;
var combo = new MyComboBox
{
Width = 100d, HorizontalAlignment = HorizontalAlignment.Left,
ToolTip = Lang.Text("Instance.Saves.Info.Modify.BeforeSave"),
SelectedValuePath = "Value", DisplayMemberPath = "Display",
};
combo.Items.Add(new { Value = 0, Display = Lang.Text("Instance.Saves.Info.AllowCommands.NotAllowed") });
combo.Items.Add(new { Value = 1, Display = Lang.Text("Instance.Saves.Info.AllowCommands.Allowed") });
combo.SelectedValue = allowCommands ? 1 : 0;
combo.SelectionChanged += async (_, _) =>
{
try
{
if (combo.SelectedValue is null) return;
await WriteLock.WaitAsync();
try
{
await SaveManager.ApplyChangesAsync(folder, new SaveChanges
{
AllowCommands = new Editable<bool>((int)combo.SelectedValue == 1),
});
}
finally
{
WriteLock.Release();
}
HintService.Hint(Lang.Text("Instance.Saves.Info.Modify.CheatSuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Info.Modify.CheatFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Modify.CheatFailed"));
}
};
AddSettingRow(Lang.Text("Instance.Saves.Info.AllowCommands"), combo);
}
private void BuildDifficultySetting(bool isHardcore, bool isLocked, int difficultyValue)
{
PanSettings.Visibility = Visibility.Visible;
var folder = PageInstanceSavesLeft.currentSave;
var combo = new MyComboBox
{
Width = 100d, HorizontalAlignment = HorizontalAlignment.Left,
ToolTip = Lang.Text("Instance.Saves.Info.Modify.BeforeSave"),
SelectedValuePath = "Value", DisplayMemberPath = "Display",
};
combo.Items.Add(new { Value = 0, Display = Lang.Text("Instance.Saves.Info.Difficulty.Peaceful") });
combo.Items.Add(new { Value = 1, Display = Lang.Text("Instance.Saves.Info.Difficulty.Easy") });
combo.Items.Add(new { Value = 2, Display = Lang.Text("Instance.Saves.Info.Difficulty.Normal") });
combo.Items.Add(new { Value = 3, Display = Lang.Text("Instance.Saves.Info.Difficulty.Hard") });
combo.SelectedValue = difficultyValue;
var lockCheckBox = new MyCheckBox
{
Text = Lang.Text("Instance.Saves.Info.LockDifficulty"),
ToolTip = Lang.Text("Instance.Saves.Info.LockDifficulty.ToolTip"),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(10d, 0d, 0d, 0d),
Checked = isLocked,
Visibility = isHardcore ? Visibility.Collapsed : Visibility.Visible,
};
var panel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Left };
panel.Children.Add(combo);
panel.Children.Add(lockCheckBox);
async Task ApplyAsync()
{
try
{
if (combo.SelectedValue is null) return;
await WriteLock.WaitAsync();
try
{
await SaveManager.ApplyChangesAsync(folder, new SaveChanges
{
Difficulty = new Editable<Difficulty>((Difficulty)(int)combo.SelectedValue),
LockDifficulty = new Editable<bool>(!isHardcore && lockCheckBox.Checked == true),
});
}
finally
{
WriteLock.Release();
}
HintService.Hint(Lang.Text("Instance.Saves.Info.Modify.DifficultySuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Info.Modify.DifficultyFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Modify.DifficultyFailed"));
}
}
combo.SelectionChanged += async (_, _) => await ApplyAsync();
lockCheckBox.Change += async (_, _) => await ApplyAsync();
AddSettingRow(Lang.Text("Instance.Saves.Info.GameDifficultyLabel"), panel);
}
private static string GameModeName(GameMode mode) => mode switch
{
GameMode.Hardcore => Lang.Text("Instance.Saves.Info.GameMode.Hardcore"),
GameMode.Creative => Lang.Text("Instance.Saves.Info.GameMode.Creative"),
GameMode.Adventure => Lang.Text("Instance.Saves.Info.GameMode.Adventure"),
GameMode.Spectator => Lang.Text("Instance.Saves.Info.GameMode.Spectator"),
_ => Lang.Text("Instance.Saves.Info.GameMode.Survival"),
};
private static void ShowHint(MyHint hint, string langKey)
{
hint.Text = Lang.Text(langKey);
hint.Visibility = Visibility.Visible;
}
private void ClearInfoTable()
{
PanList.Children.Clear();
PanList.RowDefinitions.Clear();
}
private void AddInfoRow(string head, string content, bool isSeed = false, string? versionName = null)
{
var headBlock = new TextBlock { Text = head, Margin = new Thickness(0d, 3d, 0d, 3d) };
var contentStack = new StackPanel { Orientation = Orientation.Horizontal };
if (isSeed && content != Lang.Text("Instance.Saves.Info.GetFailed"))
{
var seedBtn = new MyTextButton { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) };
seedBtn.Click += (_, _) =>
{
try
{
ModBase.ClipboardSet(content);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Info.Error.ClipboardFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Error.ClipboardFailed"));
}
};
contentStack.Children.Add(seedBtn);
var chunkbaseBtn = new MyIconButton
{
SvgIcon = "lucide/external-link",
Width = 22d,
Height = 22d,
ToolTip = Lang.Text("Instance.Saves.Info.Chunkbase.ToolTip"),
};
chunkbaseBtn.Click += (_, _) => OpenChunkbase(content, versionName);
contentStack.Children.Add(chunkbaseBtn);
}
else
{
contentStack.Children.Add(new TextBlock { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) });
}
PanList.Children.Add(headBlock);
PanList.Children.Add(contentStack);
var rowDef = new RowDefinition();
PanList.RowDefinitions.Add(rowDef);
var rowIndex = PanList.RowDefinitions.IndexOf(rowDef);
Grid.SetRow(headBlock, rowIndex);
Grid.SetColumn(headBlock, 0);
Grid.SetRow(contentStack, rowIndex);
Grid.SetColumn(contentStack, 2);
}
private void AddSettingRow(string head, UIElement control)
{
var rowIndex = PanSettingsList.RowDefinitions.Count;
PanSettingsList.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1d, GridUnitType.Auto) });
var headBlock = new TextBlock { Text = head, Margin = new Thickness(0d, 3d, 0d, 3d) };
Grid.SetRow(headBlock, rowIndex);
Grid.SetColumn(headBlock, 0);
Grid.SetRow(control, rowIndex);
Grid.SetColumn(control, 2);
PanSettingsList.Children.Add(headBlock);
PanSettingsList.Children.Add(control);
PanSettingsList.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8d, GridUnitType.Pixel) });
}
private static void OpenChunkbase(string seed, string? versionName)
{
try
{
if (versionName is null)
{
ModBase.Log(
Lang.Text("Instance.Saves.Info.Chunkbase.UnknownVersion"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Chunkbase.UnknownVersion"));
return;
}
if (versionName.Any(char.IsLetter))
{
ModBase.Log(
Lang.Text("Instance.Saves.Info.Chunkbase.PreviewVersion", versionName),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Chunkbase.PreviewVersion", versionName));
return;
}
var usedVersion = versionName.StartsWith("1.21")
? versionName.Replace(".", "_")
: versionName.Contains('.')
? string.Join("_", versionName.Split('.').Take(2))
: versionName.Replace(".", "_");
ModBase.OpenWebsite(
$"https://www.chunkbase.com/apps/seed-map#seed={seed}&platform=java_{usedVersion}&dimension=overworld");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Info.Error.ChunkbaseFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Info.Error.ChunkbaseFailed"));
}
}
}
@@ -0,0 +1,31 @@
<local:MyPageLeft x:Class="PCL.PageInstanceSavesLeft"
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" HorizontalAlignment="Left">
<StackPanel Margin="0,12,0,0" Name="PanItem">
<local:MyListItem x:Name="ItemInfo" IsScaleAnimationEnabled="False" Checked="True" Type="RadioBox" Tag="0"
MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="{DynamicResource Instance.Saves.Left.Info}"
LogoScale="1"
SvgIcon="lucide/info" />
<local:MyListItem x:Name="ItemDatapack" Title="{DynamicResource Instance.Saves.Left.Datapack}" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="1"
MinPaddingRight="35" Height="36" VerticalAlignment="Top" LogoScale="0.91"
SvgIcon="lucide/file-archive">
<local:MyListItem.Buttons>
<x:Array Type="{x:Type local:MyIconButton}">
<local:MyIconButton Tag="2" ToolTip="{DynamicResource Common.Action.Refresh}" ToolTipService.Placement="Right"
ToolTipService.InitialShowDelay="200" ToolTipService.VerticalOffset="-1"
Click="RefreshButton_Click" LogoScale="0.85"
SvgIcon="lucide/refresh-cw" />
</x:Array>
</local:MyListItem.Buttons>
</local:MyListItem>
<TextBlock Text="{DynamicResource Instance.Saves.Left.QuickActions}" Margin="13,6,5,4" Opacity="0.6" FontSize="12" />
<local:MyListItem x:Name="BtnOpenFolder" Title="{DynamicResource Common.Action.OpenFolder}" IsScaleAnimationEnabled="False" Type="RadioBox"
MinPaddingRight="35" Height="36" VerticalAlignment="Top" LogoScale="0.8"
SvgIcon="lucide/folder-open" />
</StackPanel>
</local:MyPageLeft>
@@ -0,0 +1,161 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageInstanceSavesLeft : IRefreshable
{
public static string currentSave;
// 初始化
private bool isLoad;
private void Page_Loaded(object sender, RoutedEventArgs e)
{
if (isLoad)
return;
isLoad = true;
}
private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
ModBase.OpenExplorer($@"{currentSave}\");
}
#region
/// <summary>
/// 当前页面的编号。从 0 开始计算。
/// </summary>
public FormMain.PageSubType pageID = FormMain.PageSubType.Default;
public PageInstanceSavesLeft()
{
InitializeComponent();
Loaded += Page_Loaded;
ItemInfo.Check += PageCheck;
ItemDatapack.Check += PageCheck;
BtnOpenFolder.Click += BtnOpenFolder_Click;
}
/// <summary>
/// 勾选事件改变页面。
/// </summary>
private void PageCheck(object sender, ModBase.RouteEventArgs e)
{
if (sender is MyListItem item && item.Tag is not null)
PageChange((FormMain.PageSubType)ModBase.Val(item.Tag));
}
public object PageGet(FormMain.PageSubType id = FormMain.PageSubType.Default)
{
if ((int)id == -1)
id = pageID;
switch (id)
{
case FormMain.PageSubType.VersionSavesInfo:
{
if (ModMain.frmInstanceSavesInfo is null)
ModMain.frmInstanceSavesInfo = new PageInstanceSavesInfo();
return ModMain.frmInstanceSavesInfo;
}
case FormMain.PageSubType.VersionSavesDatapack:
{
if (ModMain.frmInstanceSavesDatapack is null)
ModMain.frmInstanceSavesDatapack = new PageInstanceSavesDatapack();
return ModMain.frmInstanceSavesDatapack;
}
default:
{
throw new Exception(Lang.Text("Instance.Saves.Left.UnknownSubPage", (int)id));
}
}
}
/// <summary>
/// 切换现有页面。
/// </summary>
public void PageChange(FormMain.PageSubType id)
{
if (pageID == id)
return;
ModAnimation.AniControlEnabled += 1;
try
{
PageChangeRun((MyPageRight)PageGet(id));
pageID = id;
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.Left.SwitchFailed", (int)id),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Saves.Left.SwitchFailed", (int)id));
}
finally
{
ModAnimation.AniControlEnabled -= 1;
}
}
private static void PageChangeRun(MyPageRight target)
{
ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter
if (target.Parent is not null)
target.SetValue(ContentPresenter.ContentProperty, null);
ModMain.frmMain.pageRight = target;
((MyPageRight)ModMain.frmMain.PanMainRight.Child).PageOnExit();
ModAnimation.AniStart(new[]
{
ModAnimation.AaCode(() =>
{
((MyPageRight)ModMain.frmMain.PanMainRight.Child).PageOnForceExit();
ModMain.frmMain.PanMainRight.Child = ModMain.frmMain.pageRight;
ModMain.frmMain.pageRight.Opacity = 0d;
}, 130),
ModAnimation.AaCode(() =>
{
// 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理
ModMain.frmMain.pageRight.Opacity = 1d;
ModMain.frmMain.pageRight.PageOnEnter();
}, 30, true)
}, "PageLeft PageChange");
}
public void RefreshButton_Click(object sender, EventArgs e) // 由边栏按钮匿名调用
{
Refresh((FormMain.PageSubType)ModBase.Val(((MyIconButton)sender).Tag));
}
public void Refresh()
{
Refresh(ModMain.frmMain.PageCurrentSub);
}
public void Refresh(FormMain.PageSubType subType)
{
switch (subType)
{
case FormMain.PageSubType.VersionSavesDatapack:
{
if (ModMain.frmInstanceSavesDatapack is null)
ModMain.frmInstanceSavesDatapack = new PageInstanceSavesDatapack();
if (ItemDatapack.Checked)
ModMain.frmInstanceSavesDatapack.Refresh();
else
ItemDatapack.Checked = true;
break;
}
}
HintService.Hint(Lang.Text("Instance.Saves.Left.Refreshing"));
}
#endregion
}
@@ -0,0 +1,69 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" x:Class="PCL.PageInstanceScreenshot"
PanScroll="{Binding ElementName=PanBack}" Grid.IsSharedSizeScope="True">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanNoPic">
<Grid Margin="20,17">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="4" Margin="0,0,0,9" HorizontalAlignment="Center"
Text="{DynamicResource Instance.Screenshot.Empty.Title}" FontSize="19"
UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle Grid.Row="1" Grid.ColumnSpan="4" HorizontalAlignment="Stretch" Height="2"
Fill="{DynamicResource ColorBrush3}" />
<TextBlock Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5"
Text="{DynamicResource Instance.Screenshot.Empty.Message}" TextWrapping="Wrap" />
<local:MyButton Grid.Row="3" Grid.Column="1" Height="35" HorizontalAlignment="Center"
x:Name="BtnOpenFolderTop" MinWidth="140"
Text="{DynamicResource Instance.Screenshot.OpenFolder}" Margin="10,10,10,0"
Padding="13,0" ColorType="Highlight" />
</Grid>
</local:MyCard>
<StackPanel Orientation="Vertical" Margin="10,25,10,10" x:Name="PanContent">
<local:MyCard Margin="15,0,15,15" Title="{DynamicResource Instance.Screenshot.QuickActions}">
<Grid Height="35" Margin="25,40,15,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton Grid.Column="0" MinWidth="140"
Text="{DynamicResource Instance.Screenshot.OpenFolder}" Padding="13,0"
Margin="0,0,20,0"
HorizontalAlignment="Left" x:Name="BtnOpenFolder" ColorType="Highlight" />
</Grid>
</local:MyCard>
<WrapPanel Margin="8,0,8,25" Name="PanList" HorizontalAlignment="Center" Orientation="Horizontal">
<!--<local:MyCard Height="220" Width="120" Margin="7" Tag="Path">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="180"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Image Grid.Row="0"/>
<StackPanel Grid.Row="1">
<local:MyIconTextButton x:Name="BtnOpen" Text="{DynamicResource Common.Action.Open}"
LogoScale="0.8" SvgIcon="lucide/folder-open" />
</StackPanel>
</Grid>
</local:MyCard>-->
</WrapPanel>
</StackPanel>
</Grid>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,340 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App;
using SearchOption = System.IO.SearchOption;
using PCL.Core.App.Localization;
using PCL.Core.UI;
namespace PCL;
public partial class PageInstanceScreenshot : IRefreshable
{
private bool _AppendLock;
private int _Offset;
private List<string> fileList = new();
private bool isLoad;
private string screenshotPath;
public PageInstanceScreenshot()
{
InitializeComponent();
Loaded += PageSetupLaunch_Loaded;
PanBack.ScrollChanged += RequireAppend;
BtnOpenFolder.Click += BtnOpenFolder_Click;
BtnOpenFolderTop.Click += BtnOpenFolder_Click;
}
void IRefreshable.Refresh()
{
RefreshSelf();
}
private void RefreshSelf()
{
var ignore = RefreshAsync();
}
public static async Task RefreshAsync()
{
if (ModMain.frmInstanceScreenshot is not null)
await ModMain.frmInstanceScreenshot.ReloadAsync();
ModMain.frmInstanceLeft.ItemScreenshot.Checked = true;
HintService.Hint(Lang.Text("Instance.Saves.Status.Refreshing"), log: false);
}
private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e)
{
// 重复加载部分
PanBack.ScrollToHome();
screenshotPath = PageInstanceLeft.McInstance.PathIndie + @"screenshots\";
if (!Directory.Exists(screenshotPath))
Directory.CreateDirectory(screenshotPath);
Dispatcher.BeginInvoke(new Func<Task>(ReloadAsync));
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
}
/// <summary>
/// 确保当前页面上的信息已正确显示。
/// </summary>
public async Task ReloadAsync()
{
ModAnimation.AniControlEnabled += 1;
PanBack.ScrollToHome();
await LoadFileListAsync();
ModAnimation.AniControlEnabled -= 1;
}
private void RefreshTip()
{
if (fileList.Count.Equals(0))
{
PanNoPic.Visibility = Visibility.Visible;
PanContent.Visibility = Visibility.Collapsed;
}
else
{
PanNoPic.Visibility = Visibility.Collapsed;
PanContent.Visibility = Visibility.Visible;
}
}
private static string[] allowedSuffix = { "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.webp", "*.tiff" };
private async Task LoadFileListAsync()
{
ModBase.Log("[Screenshot] 刷新截图文件");
fileList.Clear();
if (Directory.Exists(screenshotPath))
{
fileList = allowedSuffix
.SelectMany(suffix => Directory.EnumerateFiles(screenshotPath, suffix, SearchOption.TopDirectoryOnly))
.OrderByDescending(f => File.GetCreationTime(f))
.ToList();
}
PanList.Children.Clear();
RefreshTip();
//FileList = FileList.Where(e => !e.ContainsF(@"\debug\")).ToList(); // 排除资源包调试输出
//FileList.Sort((a, b) => new FileInfo(a).CreationTime > new FileInfo(b).CreationTime);
ModBase.Log("[Screenshot] 共发现 " + fileList.Count + " 个截图文件");
if (fileList.Count == 0)
return;
await ListAppendAsync(20, 0);
}
private void RequireAppend(object sender, ScrollChangedEventArgs e)
{
if (fileList.Count != 0 && !_AppendLock && PanBack.VerticalOffset + PanBack.ViewportHeight >= PanBack.ExtentHeight)
{
Dispatcher.BeginInvoke(new Func<Task>(async () => await ListAppendAsync()));
}
}
private async Task ListAppendAsync(int count = 20, int offset = -1)
{
_AppendLock = true;
if (offset == -1)
{
if (_Offset * count > fileList.Count)
return;
offset = _Offset + 1;
_Offset += 1;
}
else
{
_Offset = offset;
}
if (count * offset > fileList.Count)
return;
for (int j = count * offset, loopTo = count * (offset + 1) - 1; j <= loopTo; j++)
{
if (j >= fileList.Count)
break;
var i = fileList.ElementAt(j);
try
{
if (!File.Exists(i))
continue; // 文件在加载途中消失了
if (File.GetAttributes(i).HasFlag(FileAttributes.Hidden))
continue; // 隐藏文件
if (new FileInfo(i).Length == 0L)
continue; // 空文件
var myCard = new MyCard
{
Margin = new Thickness(7),
Tag = i,
ToolTip = i.Replace(screenshotPath, "") // 适配高清截图模组
};
var grid = new Grid();
myCard.Children.Add(grid);
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(9d) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(120d) });
grid.RowDefinitions.Add(new RowDefinition());
// 图片
var image = new Image();
image.Source = await Task.Run(() =>
{
var bitmapImage = new BitmapImage();
var loadSource = i;
using (var fs = new FileStream(loadSource, FileMode.Open, FileAccess.Read))
{
bitmapImage.BeginInit();
bitmapImage.DecodePixelHeight = 200;
bitmapImage.DecodePixelWidth = 400;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = fs;
bitmapImage.EndInit();
bitmapImage.Freeze();
}
return bitmapImage;
});
image.Stretch = Stretch.Uniform; // 使图片自适应控件大小
image.Cursor = Cursors.Hand;
image.MouseLeftButtonDown += (sender, e) =>
{
try
{
Basics.OpenPath(i);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Screenshot.OpenFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Screenshot.OpenFailed"));
}
}; // 使用系统默认程序打开
Grid.SetRow(image, 1);
grid.Children.Add(image);
// 按钮
var stackPanel = new StackPanel();
stackPanel.Orientation = Orientation.Horizontal;
stackPanel.HorizontalAlignment = HorizontalAlignment.Center;
stackPanel.Margin = new Thickness(3d, 5d, 3d, 5d);
Grid.SetRow(stackPanel, 2);
grid.Children.Add(stackPanel);
var btnOpen = new MyIconTextButton
{
Name = "BtnOpen",
Text = Lang.Text("Common.Action.Open"),
LogoScale = 0.8d,
SvgIcon = "lucide/folder-open",
Tag = i
};
btnOpen.Click += (s, ev) => BtnOpen_Click((MyIconTextButton)s, ev);
stackPanel.Children.Add(btnOpen);
var btnDelete = new MyIconTextButton
{
Name = "BtnDelete",
Text = Lang.Text("Common.Action.Delete"),
LogoScale = 0.8d,
SvgIcon = "lucide/trash-2",
Tag = i
};
btnDelete.Click += (s, ev) => BtnDelete_Click((MyIconTextButton)s, ev);
stackPanel.Children.Add(btnDelete);
var btnCopy = new MyIconTextButton
{
Name = "BtnCopy",
Text = Lang.Text("Common.Action.Copy"),
LogoScale = 0.8d,
SvgIcon = "lucide/copy",
Tag = i
};
btnCopy.Click += (s, ev) => BtnCopy_Click((MyIconTextButton)s, ev);
stackPanel.Children.Add(btnCopy);
PanList.Children.Add(myCard);
myCard.Opacity = 0d;
ModAnimation.AniStart(new[] { ModAnimation.AaOpacity(myCard, 1d, 200) });
}
catch (Exception ex)
{
ModBase.Log(ex, $"[Screenshot] 创建 {i} 截图预览失败,图像可能损坏");
}
}
_AppendLock = false;
}
private void RemoveItem(string path)
{
try
{
foreach (var i in PanList.Children)
if (((MyCard)i).Tag.Equals(path))
{
PanList.Children.Remove((UIElement)i);
break;
}
fileList.Remove(path);
}
catch (Exception ex)
{
ModBase.Log(ex, "未能找到对应 UI");
}
}
private string GetPathFromSender(MyIconTextButton sender)
{
return (string)sender.Tag;
}
private void BtnOpen_Click(MyIconTextButton sender, EventArgs e)
{
ModBase.OpenExplorer(GetPathFromSender(sender));
}
private void BtnDelete_Click(MyIconTextButton sender, EventArgs e)
{
var path = GetPathFromSender(sender);
try
{
FileSystem.DeleteFile(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
RemoveItem(path);
RefreshTip();
HintService.Hint(Lang.Text("Instance.Screenshot.Deleted"));
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Screenshot.DeleteFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Screenshot.DeleteFailed"));
}
}
private void BtnCopy_Click(MyIconTextButton sender, EventArgs e)
{
var imagePath = GetPathFromSender(sender);
if (File.Exists(imagePath))
{
var tryTime = 0;
while (tryTime <= 5)
try
{
ModBase.Log("[Screenshot] 尝试复制" + imagePath + "到剪贴板");
Clipboard.SetImage(new BitmapImage(new Uri(imagePath)));
HintService.Hint(Lang.Text("Instance.Screenshot.CopiedToClipboard"));
tryTime = 6;
return;
}
catch (Exception ex)
{
tryTime += 1;
ModBase.Log(ex, $"[Screenshot]第 {tryTime} 次复制尝试失败");
}
HintService.Hint(Lang.Text("Instance.Screenshot.CopyFailed"), HintType.Error);
}
else
{
HintService.Hint(Lang.Text("Instance.Screenshot.FileNotFound"));
}
}
private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e)
{
if (!Directory.Exists(screenshotPath))
Directory.CreateDirectory(screenshotPath);
ModBase.OpenExplorer(screenshotPath);
}
}
@@ -0,0 +1,72 @@
<local:MyPageRight x:Class="PCL.PageInstanceServer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d"
PanScroll="{Binding ElementName=PanBack}" Grid.IsSharedSizeScope="True">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" x:Name="PanBack">
<Grid>
<local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40" x:Name="PanNoServer">
<Grid Margin="20,17">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="4" Margin="0,0,0,9" HorizontalAlignment="Center"
Text="{DynamicResource Instance.Server.Empty.Title}" FontSize="19"
UseLayoutRounding="True" SnapsToDevicePixels="True"
Foreground="{DynamicResource ColorBrush3}" />
<Rectangle Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4" HorizontalAlignment="Stretch"
Height="2" Fill="{DynamicResource ColorBrush3}" />
<TextBlock Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5"
Text="{DynamicResource Instance.Server.Empty.Message}" TextWrapping="Wrap" />
<local:MyButton Grid.Row="3" Grid.Column="1" Height="35" MinWidth="140"
Text="{DynamicResource Instance.Server.RefreshInfo}"
Padding="13,0" Margin="10,10,10,0" HorizontalAlignment="Center"
Click="BtnRefresh_Click" ColorType="Highlight" />
<local:MyButton Grid.Row="3" Grid.Column="2" Height="35" HorizontalAlignment="Center"
Click="BtnAddServer_Click" x:Name="BtnAddServerTop" MinWidth="140"
Text="{DynamicResource Instance.Server.AddServer}" Margin="10,10,10,0"
Padding="13,0" />
</Grid>
</local:MyCard>
<StackPanel Margin="10,25,10,10">
<!-- 页面标题 -->
<local:MyCard Title="{DynamicResource Instance.Server.QuickActions}" Margin="15,0,15,15"
x:Name="PanContent">
<Grid Height="35" Margin="25,40,23,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
<ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
</Grid.ColumnDefinitions>
<local:MyButton Grid.Column="0" MinWidth="140"
Text="{DynamicResource Instance.Server.RefreshAll}" Padding="13,0"
Margin="0,0,20,0"
HorizontalAlignment="Left" Click="BtnRefresh_Click" ColorType="Highlight" />
<local:MyButton Grid.Column="1" MinWidth="140"
Text="{DynamicResource Instance.Server.AddServer}" Padding="13,0"
Margin="0,0,20,0"
HorizontalAlignment="Left" Click="BtnAddServer_Click" />
</Grid>
</local:MyCard>
<!-- 服务器列表 -->
<StackPanel x:Name="PanServers" Margin="15,0,15,15">
<!-- 服务器项目将动态添加到这里 -->
</StackPanel>
</StackPanel>
</Grid>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,498 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using FluentValidation;
using fNbt;
using PCL.Core.Link.McPing;
using PCL.Core.Link.McPing.Model;
using PCL.Core.Minecraft;
using PCL.Core.Utils.Validate;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageInstanceServer : MyPageRight
{
private const int debounceInterval = 2000;
public static readonly List<MinecraftServerInfo> serverList = new();
private static readonly List<ServerCard> serverCardList = new();
private CancellationTokenSource _cts;
private DateTime _lastRefresh = DateTime.MinValue;
public PageInstanceServer()
{
InitializeComponent();
Loaded += PageLoaded;
IsVisibleChanged += PageInstanceServer_IsVisibleChanged;
}
private async void PageLoaded(object e, RoutedEventArgs sender)
{
serverList.Clear();
serverCardList.Clear();
PanServers.Children.Clear();
await LoadServersFromFileAsync();
RefreshTip();
foreach (var server in serverList)
{
var serverCard = new ServerCard();
serverCard.RemoveServer += RemoveServerEvent;
serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b);
serverCard.UpdateServerInfo(server);
serverCardList.Add(serverCard);
PanServers.Children.Add(serverCard);
}
PingAllServers();
}
private void PageInstanceServer_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (!IsVisible)
if (_cts is not null)
{
_cts.Cancel();
_cts.Dispose(); // 清理旧的 CancellationTokenSource
_cts = null;
}
}
private async void RemoveServerEvent(object sender, EventArgs e)
{
// Get server index
var index = PanServers.Children.IndexOf((UIElement)sender);
if (index < 0)
{
HintService.Hint(Lang.Text("Instance.Server.IndexNotFound"), HintType.Error);
return;
}
// Read NBT file
var nbtData =
await NbtFileHandler.ReadTagInNbtFileAsync<NbtList>(
Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"), "servers");
if (nbtData is null)
{
HintService.Hint(Lang.Text("Instance.Server.ReadDataFailed"), HintType.Error);
return;
}
// Remove server from NBT data
nbtData.RemoveAt(index);
var clonedNbtData = (NbtList)nbtData.Clone();
// Write back to NBT file
if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData,
Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat")))
{
HintService.Hint(Lang.Text("Instance.Server.WriteDataFailed"), HintType.Error);
return;
}
// Remove server from list and UI
serverList.RemoveAt(index);
serverCardList.Remove((ServerCard)sender);
if (serverList.Count == 0) RefreshTip();
// Remove UI element
PanServers.Children.Remove((UIElement)sender);
// Success message
HintService.Hint(Lang.Text("Instance.Server.Removed"), HintType.Success);
}
private async void EditServer(object sender, ServerCard.ResultEventArgs e)
{
// Read NBT file
var nbtData =
await NbtFileHandler.ReadTagInNbtFileAsync<NbtList>(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"),
"servers");
if (nbtData is null)
{
HintService.Hint(Lang.Text("Instance.Server.ReadDataFailed"), HintType.Error);
return;
}
// Get server index
var index = PanServers.Children.IndexOf((UIElement)sender);
if (index < 0 || index >= nbtData.Count)
{
HintService.Hint(Lang.Text("Instance.Server.IndexNotFound"), HintType.Error);
return;
}
// Verify server data
var server = nbtData[index] as NbtCompound;
// Update server data
server["name"] = new NbtString("name", e.Param1);
server["ip"] = new NbtString("ip", e.Param2);
// Write updated NBT data
var clonedNbtData = (NbtList)nbtData.Clone();
if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData,
Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat")))
{
HintService.Hint(Lang.Text("Instance.Server.WriteDataFailed"), HintType.Error);
return;
}
var serverCard = sender as ServerCard;
serverCard.server.Name = e.Param1;
serverCard.server.Address = e.Param2;
await serverCard.RefreshServerStatusAsync(true);
// Success message
HintService.Hint(Lang.Text("Instance.Server.Updated"), HintType.Success);
}
/// <summary>
/// 刷新服务器列表
/// </summary>
public async void RefreshServers()
{
ModBase.Log("刷新服务器列表");
try
{
// 读取服务器信息
await LoadServersFromFileAsync();
// 在UI线程中更新界面
ModBase.RunInUi(() => UpdateServerUi());
// 异步ping所有服务器
PingAllServers();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Server.RefreshFailed"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Server.RefreshFailed"));
ModBase.RunInUi(() => HintService.Hint(
Lang.Text("Instance.Server.RefreshFailed.WithDetail", ex.ToString()),
HintType.Error));
}
}
private void BtnRefresh_Click(object sender, MouseButtonEventArgs e)
{
if ((DateTime.Now - _lastRefresh).TotalMilliseconds < debounceInterval)
{
HintService.Hint(Lang.Text("Instance.Server.NoFrequentRefresh"));
return;
}
_lastRefresh = DateTime.Now;
HintService.Hint(Lang.Text("Instance.Server.RefreshingList"));
try
{
RefreshServers();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Server.RefreshFailed"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Server.RefreshFailed"));
HintService.Hint(
Lang.Text("Instance.Server.RefreshFailed.WithDetail", ex.ToString()),
HintType.Error);
}
}
private async void BtnAddServer_Click(object sender, MouseButtonEventArgs e)
{
var result = GetServerInfo(new MinecraftServerInfo { Name = Lang.Text("Instance.Server.DefaultName"), Address = "" });
if (result.Success)
{
var newServer = new MinecraftServerInfo
{
Name = result.Name,
Address = result.Address,
Status = ServerStatus.Unknown
};
serverList.Add(newServer);
RefreshTip();
var serverCard = new ServerCard();
serverCard.RemoveServer += RemoveServerEvent;
serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b);
serverCard.UpdateServerInfo(newServer);
serverCardList.Add(serverCard);
PanServers.Children.Add(serverCard);
await serverCard.RefreshServerStatusAsync(false);
var serversDatPath = Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat");
NbtList nbtData;
if (!File.Exists(serversDatPath))
{
nbtData = new NbtList("servers", NbtTagType.Compound);
RefreshTip();
}
else
{
nbtData = await NbtFileHandler.ReadTagInNbtFileAsync<NbtList>(serversDatPath, "servers");
}
if (nbtData is not null)
{
var server = new NbtCompound();
server["name"] = new NbtString("name", result.Name);
server["ip"] = new NbtString("ip", result.Address);
if (nbtData.Count == 0) nbtData.ListType = NbtTagType.Compound;
nbtData.Add(server);
var clonedNbtData = (NbtList)nbtData.Clone();
await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, serversDatPath);
}
}
}
public static (string Name, string Address, bool Success) GetServerInfo(MinecraftServerInfo server)
{
var newName = ModMain.MyMsgBoxInput(Lang.Text("Instance.Server.EditTitle"), Lang.Text("Instance.Server.NamePrompt"), server.Name,
[new NullOrWhiteSpaceValidator()]);
if (string.IsNullOrEmpty(newName)) return (string.Empty, string.Empty, false);
var newAddress = ModMain.MyMsgBoxInput(Lang.Text("Instance.Server.EditTitle"), Lang.Text("Instance.Server.AddressPrompt"), server.Address,
[new NullOrWhiteSpaceValidator()]);
if (string.IsNullOrEmpty(newAddress)) return (string.Empty, string.Empty, false);
return (newName, newAddress, true);
}
/// <summary>
/// 从servers.dat文件读取服务器信息
/// </summary>
private async Task LoadServersFromFileAsync()
{
serverList.Clear();
var serversFile = Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat");
if (!File.Exists(serversFile))
return;
try
{
// 读取NBT格式的servers.dat文件
var nbtData = await NbtFileHandler.ReadTagInNbtFileAsync<NbtList>(serversFile, "servers");
ParseServersFromNBT(nbtData);
}
catch (Exception ex)
{
ModBase.Log(ex, Lang.Text("Instance.Server.ReadFileFailed"));
}
}
/// <summary>
/// 解析NBT格式的服务器数据
/// </summary>
private void ParseServersFromNBT(NbtList serversList)
{
if (serversList is not null)
{
ModBase.Log($"Found {serversList.Count} servers:");
// 遍历 servers 列表中的每个服务器
for (int i = 0, loopTo = serversList.Count - 1; i <= loopTo; i++)
{
var server = serversList[i] as NbtCompound;
if (server is not null)
{
// 提取服务器信息
// Dim hidden As Byte = If(server.Get(Of NbtByte)("hidden")?.Value, 0)
var ip = server.Get<NbtString>("ip")?.Value ?? "Unknown";
var name = server.Get<NbtString>("name")?.Value ?? "Unknown";
var iconBase64 = server.Get<NbtString>("icon")?.Value;
ModBase.Log($"服务器 {i + 1}:");
ModBase.Log($" 名字: {name}");
ModBase.Log($" IP: {ip}");
// Log($" Hidden: {If(hidden = 1, "Yes", "No")}")
serverList.Add(new MinecraftServerInfo
{
Name = name,
Address = ip,
Status = ServerStatus.Unknown,
Icon = iconBase64
});
}
}
}
else
{
ModBase.Log("No 'servers' list found in servers.dat.");
}
}
/// <summary>
/// 更新服务器UI显示
/// </summary>
private void UpdateServerUi()
{
PanServers.Children.Clear();
RefreshTip();
foreach (var server in serverList)
{
var serverCard = new ServerCard();
serverCard.RemoveServer += RemoveServerEvent;
serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b);
serverCard.UpdateServerInfo(server);
serverCardList.Add(serverCard);
PanServers.Children.Add(serverCard);
}
}
private void RefreshTip()
{
if (serverList.Count == 0)
{
ModBase.Log(Lang.Text("Instance.Server.NoServersFound"));
PanNoServer.Visibility = Visibility.Visible;
PanContent.Visibility = Visibility.Collapsed;
PanServers.Visibility = Visibility.Collapsed;
return;
}
ModBase.Log(Lang.Text("Instance.Server.FoundServers"));
PanNoServer.Visibility = Visibility.Collapsed;
PanContent.Visibility = Visibility.Visible;
PanServers.Visibility = Visibility.Visible;
}
private async void PingAllServers()
{
if (_cts is not null)
{
_cts.Cancel();
_cts.Dispose();
}
_cts = new CancellationTokenSource();
var token = _cts.Token;
var semaphore = new SemaphoreSlim(5); // 限制最多 5 个并发任务
var tasks = new List<Task>();
try
{
var snapshot = serverCardList.ToList();
foreach (var server in snapshot)
{
var currentServer = server;
await semaphore.WaitAsync(token);
tasks.Add(Task.Run(async () =>
{
try
{
await currentServer.RefreshServerStatusAsync(false, token);
}
catch (Exception ex)
{
ModBase.Log(ex, $"Ping 服务器失败: {currentServer}");
}
finally
{
semaphore.Release();
}
}, token));
}
await Task.WhenAll(tasks); // 等待所有任务完成
}
catch (OperationCanceledException ex)
{
ModBase.Log("PingAllServers 被取消", ModBase.LogLevel.Debug);
}
catch (Exception ex)
{
ModBase.Log(ex, "PingAllServers 失败");
}
}
/// <summary>
/// ping单个服务器
/// </summary>
public static async Task<MinecraftServerInfo> PingServerAsync(MinecraftServerInfo server, CancellationToken token)
{
try
{
var addr = await ServerAddressResolver.GetResolvedServerAddressAsync(server.Address, token);
using (var query = McPingServiceFactory.CreateService(addr.Host, addr.Ip, addr.Port))
{
McPingResult? result;
ModBase.Log("Pinging server: " + server.Address + ":" + addr.Port);
result = await query.PingAsync(token); // 传递 token
ModBase.Log("Ping result: " + (result is not null ? "Success" : "Failed"));
if (result is not null)
{
server.Status = ServerStatus.Online;
server.PlayerCount = result.Players.Online;
server.MaxPlayers = result.Players.Max;
server.Description = result.Description ?? string.Empty;
server.Version = result.Version.Name;
server.Ping = (int)result.Latency;
server.Icon = result.Favicon;
}
else
{
server.Status = ServerStatus.Offline;
}
}
}
catch (OperationCanceledException ex)
{
server.Status = ServerStatus.Offline;
ModBase.Log("Ping 服务器被取消: " + server.Address, ModBase.LogLevel.Debug);
}
catch (Exception ex)
{
server.Status = ServerStatus.Offline;
ModBase.Log(ex, $"Ping 服务器失败: {server.Address}:{server.Port}");
}
return server;
}
}
/// <summary>
/// Minecraft服务器信息类
/// </summary>
public class MinecraftServerInfo
{
public string Name { get; set; }
public string Address { get; set; }
public int Port { get; set; } = 25565;
public ServerStatus Status { get; set; } = ServerStatus.Unknown;
public int PlayerCount { get; set; }
public int MaxPlayers { get; set; }
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public int Ping { get; set; }
public string Icon { get; set; } = "";
}
/// <summary>
/// 服务器状态枚举
/// </summary>
public enum ServerStatus
{
Unknown,
Online,
Offline,
Pinging
}
@@ -0,0 +1,410 @@
<local:MyPageRight
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL"
xmlns:val="https://ce.pclc.cc/core/utils/validate"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:System="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d" x:Class="PCL.PageInstanceSetup"
PanScroll="{Binding ElementName=PanBack}">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<StackPanel x:Name="PanMain" Margin="25,25,25,25">
<local:MyHint Text="{DynamicResource Instance.Setup.Hint}" Theme="Blue" Margin="0,0,0,15"
CanClose="True" RelativeSetup="HintIndieSetup" />
<local:MyCard x:Name="CardArgument" Margin="0,0,0,15"
Title="{DynamicResource Setup.Launch.Options.Title}">
<StackPanel Margin="25,40,25,21">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Setup.Options.InstanceIsolation}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="ComboArgumentIndieV2" Grid.Row="0" Grid.ColumnSpan="2"
Tag="VersionArgumentIndieV2" Grid.Column="1">
<local:MyComboBoxItem Content="{DynamicResource Instance.Setup.Options.InstanceIsolation.Enabled}"
ToolTipService.InitialShowDelay="0"
ToolTipService.BetweenShowDelay="0"
ToolTip="{DynamicResource Instance.Setup.Options.InstanceIsolation.Enabled.ToolTip}"
ToolTipService.HorizontalOffset="100" />
<local:MyComboBoxItem Content="{DynamicResource Common.Action.Close}"
ToolTipService.InitialShowDelay="0"
ToolTipService.BetweenShowDelay="0"
ToolTip="{DynamicResource Instance.Setup.Options.InstanceIsolation.Disabled.ToolTip}"
ToolTipService.HorizontalOffset="100" />
</local:MyComboBox>
<TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Options.WindowTitle.Label}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2"
Tag="VersionArgumentTitle" Grid.Column="1" IsEditable="True"
IsTextSearchEnabled="False"
ToolTip="{DynamicResource Instance.Setup.Options.WindowTitle.ToolTip}">
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Options.WindowTitle.SampleFormat}" />
</local:MyComboBox>
<local:MyCheckBox x:Name="CheckArgumentTitleEmpty" Margin="0,0,0,9" Grid.Row="4"
Grid.ColumnSpan="3"
Text="{DynamicResource Instance.Setup.Options.WindowTitle.Default}"
Tag="VersionArgumentTitleEmpty"
Grid.Column="0"
ToolTip="{DynamicResource Instance.Setup.Options.WindowTitle.Default.ToolTip}" />
<TextBlock Grid.Row="6" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Options.CustomInfo.Label}"
Margin="0,0,25,0" />
<local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="6" Grid.ColumnSpan="2"
Tag="VersionArgumentInfo" Grid.Column="1"
HintText="{DynamicResource Instance.Setup.FollowGlobal}"
ToolTip="{DynamicResource Instance.Setup.Options.CustomInfo.ToolTip}">
<local:MyTextBox.ValidateRules>
<val:BlacklistValidator>
<val:BlacklistValidator.Blacklist>
<System:String>"</System:String>
<System:String>“</System:String>
<System:String>”</System:String>
</val:BlacklistValidator.Blacklist>
</val:BlacklistValidator>
</local:MyTextBox.ValidateRules>
</local:MyTextBox>
<TextBlock VerticalAlignment="Center" Grid.Row="8" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Setup.Options.Java}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="ComboArgumentJava" Grid.ColumnSpan="2" Grid.Row="8" Grid.Column="1"
MaxDropDownHeight="240"
ToolTip="{DynamicResource Instance.Setup.Options.Java.ToolTip}">
<ComboBoxItem Content="{DynamicResource Instance.Setup.Options.Java.Loading}" IsSelected="True" />
</local:MyComboBox>
</Grid>
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Setup.Launch.Memory.Title}">
<StackPanel Margin="25,40,25,15">
<local:MyHint x:Name="LabRamWarn" Text="{DynamicResource Setup.Launch.Memory.Warning.Java32Bit}"
Theme="Yellow" Margin="0,0,0,12" />
<local:MyHint x:Name="HintRamTooHigh" Visibility="Collapsed" Height="30" Theme="Yellow"
Text="{DynamicResource Setup.Launch.Memory.Warning.TooMuch}" Margin="0,0,0,12"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
<Grid Margin="0,0,0,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="9" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<local:MyRadioBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="{DynamicResource Instance.Setup.FollowGlobal}"
Checked="True"
x:Name="RadioRamType2"
Tag="VersionRamType/2"
Margin="0,0,20,9" />
<local:MyRadioBox
Grid.Row="1" Grid.Column="0"
Text="{DynamicResource Setup.Launch.Memory.Auto}"
Width="110"
x:Name="RadioRamType0"
Tag="VersionRamType/0"
Margin="0,0,20,0"
ToolTip="{DynamicResource Setup.Launch.Memory.Auto.ToolTip}"
ToolTipService.Placement="Right"
ToolTipService.HorizontalOffset="-10"
ToolTipService.VerticalOffset="-3" />
<local:MyRadioBox
Grid.Row="3" Grid.Column="0"
Text="{DynamicResource Common.Option.Customize}"
Width="110"
x:Name="RadioRamType1"
Tag="VersionRamType/1"
Margin="0,0,20,0" />
<local:MySlider
Grid.Row="3" Grid.Column="1"
IsEnabled="False"
x:Name="SliderRamCustom"
Tag="VersionRamCustom"
MaxValue="49"
Value="13"
Margin="0,0,12,0" />
</Grid>
<Grid x:Name="PanRamDisplay" Margin="0,11,2,0" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="ColumnRamUsed" Width="4.7*" />
<ColumnDefinition x:Name="ColumnRamGame" Width="2.5*" />
<ColumnDefinition x:Name="ColumnRamEmpty" Width="0.7*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="4" />
<RowDefinition />
</Grid.RowDefinitions>
<Rectangle x:Name="RectRamUsed" Grid.Row="1" StrokeThickness="0">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="{DynamicResource ColorObject3}" Offset="0" />
<GradientStop Color="{DynamicResource ColorObject2}" Offset="0.5" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Row="1" StrokeThickness="0" Grid.Column="1"
Fill="{DynamicResource ColorBrush3}" Opacity="0.5" x:Name="RectRamGame" />
<Rectangle x:Name="RectRamEmpty" Grid.Row="1" StrokeThickness="0" Grid.Column="2"
Fill="{DynamicResource ColorBrush6}" Opacity="0.7" />
<TextBlock x:Name="LabRamUsedTitle" Text="{DynamicResource Instance.Setup.Memory.Used}"
Grid.ColumnSpan="3" Opacity="0.7"
TextTrimming="None" Margin="2,0,0,5" FontSize="11" HorizontalAlignment="Left" />
<TextBlock x:Name="LabRamGameTitle" Text="{DynamicResource Setup.Launch.Memory.GameAllocation}"
Opacity="0.7" Grid.ColumnSpan="3"
TextTrimming="None" Margin="2,0,0,5" FontSize="11" HorizontalAlignment="Left" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Grid.ColumnSpan="3" Margin="2,3,0,0"
HorizontalAlignment="Left">
<TextBlock x:Name="LabRamUsed" Text="4.7 GiB" FontSize="16"
Foreground="{DynamicResource ColorBrushMemory}" TextTrimming="None" />
<TextBlock x:Name="LabRamTotal" Text=" / 7.9 GiB" FontSize="16"
Foreground="{DynamicResource ColorBrushMemory}" TextTrimming="None" />
</StackPanel>
<TextBlock Grid.Row="2" x:Name="LabRamGame" Text="2.5 GiB" Grid.ColumnSpan="3"
TextTrimming="None" Margin="2,3,0,0" FontSize="16"
Foreground="{DynamicResource ColorBrushMemory}" HorizontalAlignment="Left" />
</Grid>
</StackPanel>
</local:MyCard>
<local:MyCard x:Name="CardServer" Margin="0,0,0,15" Title="{DynamicResource Instance.Setup.Server.Title}">
<StackPanel Margin="25,42,25,18">
<local:MyHint x:Name="HintServerLoginLock"
Text="{DynamicResource Instance.Setup.Server.LoginLockHint}" IsWarn="True"
Margin="0,0,0,13" Visibility="Collapsed" />
<local:MyHint x:Name="LabServerAuthServerSecurityVerify"
Text="{DynamicResource Instance.Setup.Server.SslWarning}"
IsWarn="False" Visibility="Collapsed" />
<local:MyHint x:Name="LabServerAuthServerSecurity"
Text="{DynamicResource Instance.Setup.Server.HttpWarning}"
Visibility="Collapsed" />
<TextBlock Name="LabServerAuthServerSecurityCL" Text="&#xA;" Visibility="Collapsed" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
<RowDefinition Height="3" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Setup.Server.RestrictLogin}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="ComboServerLoginRequire" Grid.Row="0" Grid.ColumnSpan="2"
Tag="VersionServerLoginRequire" Grid.Column="1">
<local:MyComboBoxItem
Content="{DynamicResource Instance.Setup.Server.LoginMethod.Unlimited}"
IsSelected="True" />
<local:MyComboBoxItem
Content="{DynamicResource Instance.Setup.Server.LoginMethod.MicrosoftOnly}" />
<local:MyComboBoxItem
Content="{DynamicResource Instance.Setup.Server.LoginMethod.AuthOnly}" />
<local:MyComboBoxItem
Content="{DynamicResource Instance.Setup.Server.LoginMethod.MicrosoftOrAuth}" />
</local:MyComboBox>
<TextBlock Name="LabServerAuthServer" Grid.Row="1" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="{DynamicResource Instance.Setup.Server.AuthServer}"
Margin="0,8,25,0" Visibility="Collapsed" />
<local:MyTextBox x:Name="TextServerAuthServer" Grid.Row="1" Grid.ColumnSpan="2"
ShowValidateResult="False" Tag="VersionServerAuthServer" Grid.Column="1"
Margin="0,8,0,0" Visibility="Collapsed"
HintText="{DynamicResource Instance.Setup.Server.AuthServer.Hint}">
<local:MyTextBox.ValidateRules>
<val:HttpValidator />
</local:MyTextBox.ValidateRules>
</local:MyTextBox>
<TextBlock Name="LabServerAuthRegister" Grid.Row="2" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="{DynamicResource Instance.Setup.Server.Register}"
Margin="0,8,25,0" Visibility="Collapsed" />
<local:MyTextBox x:Name="TextServerAuthRegister" Grid.Row="2" Grid.ColumnSpan="2"
ShowValidateResult="False" Tag="VersionServerAuthRegister" Grid.Column="1"
Margin="0,8,0,0" Visibility="Collapsed"
HintText="{DynamicResource Instance.Setup.Server.Register.Hint}">
<local:MyTextBox.ValidateRules>
<val:HttpValidator AllowsNullOrEmpty="True" />
</local:MyTextBox.ValidateRules>
</local:MyTextBox>
<TextBlock Name="LabServerAuthName" Grid.Row="3" VerticalAlignment="Center"
HorizontalAlignment="Left" Text="{DynamicResource Instance.Setup.Server.Name}"
Margin="0,8,25,0" Visibility="Collapsed" />
<local:MyTextBox x:Name="TextServerAuthName" Grid.Row="3" Grid.ColumnSpan="2"
ShowValidateResult="False" Tag="VersionServerAuthName" Grid.Column="1"
Margin="0,8,0,0" Visibility="Collapsed"
HintText="{DynamicResource Instance.Setup.Server.Name.Hint}" />
<TextBlock VerticalAlignment="Center" Grid.Row="5" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Setup.Server.AutoJoin}"
Margin="0,0,25,0" />
<local:MyTextBox x:Name="TextServerEnter" Grid.ColumnSpan="2" Grid.Row="5"
Tag="VersionServerEnter" Grid.Column="1"
ToolTip="{DynamicResource Instance.Setup.Server.AutoJoin.ToolTip}">
<local:MyTextBox.ValidateRules>
<val:BlacklistValidator>
<val:BlacklistValidator.Blacklist>
<System:String>"</System:String>
<System:String>“</System:String>
<System:String>”</System:String>
<System:String>http://</System:String>
<System:String>https://</System:String>
</val:BlacklistValidator.Blacklist>
</val:BlacklistValidator>
</local:MyTextBox.ValidateRules>
</local:MyTextBox>
</Grid>
<local:MyButton x:Name="BtnServerAuthLittle" Height="35" MinWidth="150" Margin="0,12,0,0"
Text="{DynamicResource Instance.Setup.Server.SetLittleSkin}"
HorizontalAlignment="Left" ColorType="Highlight" />
<local:MyButton x:Name="BtnServerAuthLock" Height="35" MinWidth="150" Margin="180,-35,0,0"
Text="{DynamicResource Instance.Setup.Server.LockLoginMethod}"
HorizontalAlignment="Left" IsEnabled="False" />
<local:MyButton x:Name="BtnServerNewProfile" Height="35" MinWidth="150" Margin="360,-35,0,0"
Text="{DynamicResource Instance.Setup.Server.CreateProfile}"
HorizontalAlignment="Left" />
</StackPanel>
</local:MyCard>
<local:MyCard x:Name="CardAdvance" Margin="0,0,0,15"
Title="{DynamicResource Setup.Launch.Advanced.Title}" IsSwapped="True" CanSwap="True">
<Grid Margin="25,40,25,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="Auto" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="28" />
<RowDefinition Height="9" />
<RowDefinition Height="31" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Advanced.Renderer.Label}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="ComboAdvanceRenderer" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Tag="VersionAdvanceRenderer">
<local:MyComboBoxItem Content="{DynamicResource Instance.Setup.FollowGlobal}"
IsSelected="True" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Advanced.Renderer.GameDefault}" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Advanced.Renderer.Software}" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Advanced.Renderer.DirectX12}" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Advanced.Renderer.Vulkan}" />
</local:MyComboBox>
<TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Advanced.JvmHead}"
Margin="0,0,25,0" />
<local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionAdvanceJvm"
Grid.Column="1" MaxLength="4000"
ToolTip="{DynamicResource Instance.Setup.Advanced.JvmArgs.ToolTip}"
HintText="{DynamicResource Instance.Setup.FollowGlobal}"
Height="100" AcceptsReturn="True" TextWrapping="Wrap"
VerticalContentAlignment="Top" Padding="0,5" VerticalScrollBarVisibility="Auto" />
<TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Advanced.GameTail}"
Margin="0,0,25,0" />
<local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="4" Grid.ColumnSpan="2" Tag="VersionAdvanceGame"
Grid.Column="1"
ToolTip="{DynamicResource Instance.Setup.Advanced.GameArgs.ToolTip}"
HintText="{DynamicResource Instance.Setup.FollowGlobal}" />
<TextBlock Grid.Row="6" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Instance.Setup.Advanced.Classpath}"
Margin="0,0,25,0" />
<local:MyTextBox x:Name="TextAdvanceClasspathHead" Grid.Row="6" Grid.ColumnSpan="2"
Tag="VersionAdvanceClasspathHead" Grid.Column="1"
ToolTip="{DynamicResource Instance.Setup.Advanced.Classpath.ToolTip}" />
<TextBlock Grid.Row="8" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Advanced.PreLaunchCommand}"
Margin="0,0,25,3" />
<local:MyTextBox x:Name="TextAdvanceRun" Grid.Row="8" Grid.ColumnSpan="2" Tag="VersionAdvanceRun"
Grid.Column="1" HintText="" Margin="0,0,0,3"
ToolTip="{DynamicResource Instance.Setup.Advanced.PreLaunchCommand.ToolTip}" />
<StackPanel Margin="0,8,0,4" Grid.Row="9" Grid.ColumnSpan="2" HorizontalAlignment="Left">
<local:MyCheckBox Height="28" x:Name="CheckAdvanceRunWait" Tag="VersionAdvanceRunWait"
Visibility="Collapsed"
Text="{DynamicResource Setup.Launch.Advanced.PreLaunchCommand.Wait}" />
<local:MyCheckBox Height="28"
Text="{DynamicResource Instance.Setup.Advanced.IgnoreJavaCompatibility}"
x:Name="CheckAdvanceJava"
Tag="VersionAdvanceJava"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Instance.Setup.Advanced.IgnoreJavaCompatibility.ToolTip}" />
<local:MyCheckBox Height="28"
Text="{DynamicResource Instance.Setup.Advanced.DisableFileVerification}"
x:Name="CheckAdvanceAssetsV2"
Tag="VersionAdvanceAssetsV2" Margin="0,0,60,0"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Instance.Setup.Advanced.DisableFileVerification.ToolTip}" />
<local:MyCheckBox Height="28" Text="{DynamicResource Instance.Setup.Advanced.UseGlobalProxy}"
x:Name="CheckAdvanceUseProxyV2"
Tag="VersionAdvanceUseProxyV2"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Instance.Setup.Advanced.UseGlobalProxy.ToolTip}" />
<local:MyCheckBox Height="28"
Text="{DynamicResource Setup.Launch.Advanced.DisableJlw}"
x:Name="CheckAdvanceDisableJLW"
Tag="VersionAdvanceDisableJLW"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Setup.Launch.Advanced.DisableJlw.ToolTip}" />
<local:MyCheckBox Height="28"
Text="{DynamicResource Setup.Launch.Advanced.DisableLF}"
x:Name="CheckAdvanceDisableLF"
Tag="VersionAdvanceDisableLF"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Setup.Launch.Advanced.DisableLF.ToolTip}" />
<local:MyCheckBox Height="28" Text="{DynamicResource Instance.Setup.Advanced.UseDebugLog4j}"
x:Name="CheckUseDebugLog4j2Config"
Tag="VersionUseDebugLog4j2Config"
ToolTipService.Placement="Right" />
<local:MyCheckBox Height="28"
Text="{DynamicResource Setup.Launch.Advanced.DisableLwjglUnsafeAgent}"
x:Name="CheckAdvanceDisableLwjglUnsafeAgent"
Tag="VersionAdvanceDisableLwjglUnsafeAgent"
ToolTipService.Placement="Right"
ToolTip="{DynamicResource Setup.Launch.Advanced.DisableLwjglUnsafeAgent.ToolTip}" />
</StackPanel>
</Grid>
</local:MyCard>
<local:MyExtraTextButton HorizontalAlignment="Center" Margin="0,-5,0,0"
x:Name="BtnSwitch" Text="{DynamicResource Instance.Setup.OpenGlobalSettings}"
LogoScale="0.9"
SvgIcon="lucide/arrow-right" />
</StackPanel>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,1053 @@
using System.IO;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using PCL.Core.App;
using PCL.Core.App.Configuration;
using PCL.Core.IO;
using PCL.Core.Minecraft;
using PCL.Core.Minecraft.Java.UserPreference;
using PCL.Core.UI;
using PCL.Core.Utils.OS;
using PCL.Core.App.Localization;
using PCL.Core.Utils;
namespace PCL;
public partial class PageInstanceSetup
{
private new bool isLoaded;
public PageInstanceSetup()
{
Loaded += PageSetupSystem_Loaded;
InitializeComponent();
ComboArgumentIndieV2.SelectionChanged += ComboArgumentIndieV2_SelectionChanged;
TextArgumentTitle.TextChanged += TextArgumentTitle_TextChanged;
TextArgumentInfo.TextChanged += TextBoxChange;
ComboArgumentJava.SelectionChanged += JavaSelectionUpdate;
RadioRamType2.Check += RadioBoxChange;
RadioRamType0.Check += RadioBoxChange;
RadioRamType1.Check += RadioBoxChange;
SliderRamCustom.Change += SliderChange;
ComboServerLoginRequire.SelectionChanged += ComboServerLogin_Changed;
TextServerAuthServer.TextChanged += TextBoxChange;
TextServerAuthServer.LostFocus += TextServerAuthServer_MouseLeave;
TextServerAuthRegister.TextChanged += TextBoxChange;
TextServerAuthName.TextChanged += TextBoxChange;
TextServerEnter.TextChanged += TextBoxChange;
BtnServerAuthLittle.Click += BtnServerAuthLittle_Click;
BtnServerAuthLock.Click += BtnServerAuthLock_Click;
BtnServerNewProfile.Click += BtnServerNewProfile_Click;
ComboAdvanceRenderer.SelectionChanged += ComboAdvanceRenderer_SelectionChanged;
TextAdvanceJvm.TextChanged += TextBoxChange;
TextAdvanceGame.TextChanged += TextBoxChange;
TextAdvanceClasspathHead.TextChanged += TextBoxChange;
TextAdvanceRun.TextChanged += TextAdvanceRun_TextChanged;
CheckAdvanceRunWait.Change += CheckBoxChange;
CheckAdvanceJava.Change += CheckBoxChange;
CheckAdvanceAssetsV2.Change += CheckBoxChange;
CheckAdvanceUseProxyV2.Change += CheckBoxChange;
CheckAdvanceDisableJLW.Change += CheckBoxChange;
CheckAdvanceDisableLF.Change += CheckBoxChange;
CheckUseDebugLog4j2Config.Change += CheckUseDebugLog4j2Config_CheckChanged;
CheckAdvanceDisableLwjglUnsafeAgent.Change += CheckBoxChange;
BtnSwitch.Click += BtnSwitch_Click;
TextServerEnter.TextChanged += TextServerEnter_Change;
ComboArgumentJava.DropDownOpened += ComboArgumentJava_DropDownOpened;
CheckArgumentTitleEmpty.Change += CheckArgumentTitleEmpty_Change;
}
private void PageSetupSystem_Loaded(object sender, RoutedEventArgs e)
{
// 重复加载部分
PanBack.ScrollToHome();
RefreshRam(false);
// 由于各个实例不同,每次都需要重新加载
ModAnimation.AniControlEnabled += 1;
Reload();
ModAnimation.AniControlEnabled -= 1;
// 非重复加载部分
if (isLoaded)
return;
isLoaded = true;
// 内存自动刷新
var timer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) };
timer.Tick += (_, _) => RefreshRam();
timer.Start();
RectRamGame.SizeChanged += (s, e) => RefreshRamText();
}
public void Reload()
{
try
{
// 启动参数
TextArgumentTitle.Text = Config.Instance.Title[PageInstanceLeft.McInstance.PathInstance];
CheckArgumentTitleEmpty.Checked = Config.Instance.UseGlobalTitle[PageInstanceLeft.McInstance.PathInstance];
TextArgumentInfo.Text = Config.Instance.TypeInfo[PageInstanceLeft.McInstance.PathInstance];
var _unused = PageInstanceLeft.McInstance.PathIndie; // 触发自动判定
ComboArgumentIndieV2.SelectedIndex = Config.Instance.IndieV2[PageInstanceLeft.McInstance.PathInstance] ? 0 : 1;
CheckArgumentTitleEmpty.Visibility = TextArgumentTitle.Text.Length > 0 ? Visibility.Collapsed : Visibility.Visible;
TextArgumentTitle.HintText = CheckArgumentTitleEmpty.Checked == true ? Lang.Text("Common.Option.Default") : Lang.Text("Instance.Setup.FollowGlobal");
RefreshJavaComboBox();
// 游戏内存
var ramType = Config.Instance.MemorySolution[PageInstanceLeft.McInstance.PathInstance];
((MyRadioBox)FindName("RadioRamType" + ramType)).Checked = true;
SliderRamCustom.Value = Config.Instance.CustomMemorySize[PageInstanceLeft.McInstance.PathInstance];
RamType(ramType);
// 服务器
TextServerEnter.Text = Config.Instance.ServerToEnter[PageInstanceLeft.McInstance.PathInstance];
ComboServerLoginRequire.SelectedIndex = Config.InstanceAuth.LoginRequirementSolution[PageInstanceLeft.McInstance.PathInstance];
comboServerLoginLast = ComboServerLoginRequire.SelectedIndex;
ServerLogin(ComboServerLoginRequire.SelectedIndex);
TextServerAuthServer.Text = Config.InstanceAuth.AuthServerAddress[PageInstanceLeft.McInstance.PathInstance];
TextServerAuthName.Text = Config.InstanceAuth.AuthServerDisplayName[PageInstanceLeft.McInstance.PathInstance];
TextServerAuthRegister.Text = Config.InstanceAuth.AuthRegisterAddress[PageInstanceLeft.McInstance.PathInstance];
// 高级设置
ComboAdvanceRenderer.SelectedIndex = Config.Instance.Renderer[PageInstanceLeft.McInstance.PathInstance];
TextAdvanceClasspathHead.Text = Config.Instance.ClasspathHead[PageInstanceLeft.McInstance.PathInstance];
TextAdvanceJvm.Text = Config.Instance.JvmArgs[PageInstanceLeft.McInstance.PathInstance];
TextAdvanceGame.Text = Config.Instance.GameArgs[PageInstanceLeft.McInstance.PathInstance];
TextAdvanceRun.Text = Config.Instance.PreLaunchCommand[PageInstanceLeft.McInstance.PathInstance];
CheckAdvanceRunWait.Checked = Config.Instance.PreLaunchCommandWait[PageInstanceLeft.McInstance.PathInstance];
CheckAdvanceDisableLwjglUnsafeAgent.Checked = Config.Instance.DisableLwjglUnsafeAgent[PageInstanceLeft.McInstance.PathInstance];
if (Config.Instance.AssetVerifySolutionV1[PageInstanceLeft.McInstance.PathInstance] == 2)
{
ModBase.Log("[Setup] 已迁移老版本的关闭文件校验设置");
Config.Instance.AssetVerifySolutionV1Config.Reset(PageInstanceLeft.McInstance.PathInstance);
Config.Instance.DisableAssetVerifyV2[PageInstanceLeft.McInstance.PathInstance] = true;
}
CheckAdvanceAssetsV2.Checked = Config.Instance.DisableAssetVerifyV2[PageInstanceLeft.McInstance.PathInstance];
CheckAdvanceUseProxyV2.Checked = Config.Instance.UseProxy[PageInstanceLeft.McInstance.PathInstance];
CheckAdvanceJava.Checked = Config.Instance.IgnoreJavaCompatibility[PageInstanceLeft.McInstance.PathInstance];
if (SystemInfo.IsArm64System)
{
CheckAdvanceDisableJLW.Checked = true;
CheckAdvanceDisableJLW.IsEnabled = false;
CheckAdvanceDisableJLW.ToolTip = Lang.Text("Setup.Launch.Advanced.DisableJlw.Arm64ToolTip");
}
else
{
CheckAdvanceDisableJLW.Checked = Config.Instance.DisableJlw[PageInstanceLeft.McInstance.PathInstance];
}
CheckUseDebugLog4j2Config.Checked = Config.Instance.UseDebugLof4j2Config[PageInstanceLeft.McInstance.PathInstance];
CheckAdvanceDisableLF.Checked = Config.Instance.DisableLF[PageInstanceLeft.McInstance.PathInstance];
}
catch (Exception ex)
{
ModBase.Log(
ex,
"重载实例独立设置时出错",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Setup.Error.OperationFailed"));
}
}
// 初始化
public void Reset()
{
try
{
if (!Config.InstanceAuth.AuthLocked[PageInstanceLeft.McInstance.PathInstance])
Config.InstanceAuth.Reset(PageInstanceLeft.McInstance.PathInstance);
Config.Instance.Reset(PageInstanceLeft.McInstance.PathInstance);
ModBase.Log("[Setup] 已初始化实例独立设置");
HintService.Hint(Lang.Text("Instance.Setup.Initialize.Success"), HintType.Success, false);
}
catch (Exception ex)
{
ModBase.Log(
ex,
"初始化实例独立设置失败",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.Setup.Error.OperationFailed"));
}
Reload();
}
// 将控件改变路由到设置改变
private static void SetByTag(string tag, object value)
=> ConfigService.TrySetValue(tag, value, PageInstanceLeft.McInstance.PathInstance);
private void RadioBoxChange(object o, ModBase.RouteEventArgs routeEventArgs)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (o is not MyRadioBox { Tag: string tag }) return;
var slash = tag.IndexOf('/');
if (slash < 0) return;
SetByTag(tag[..slash], int.Parse(tag[(slash + 1)..]));
}
private void TextBoxChange(object o, TextChangedEventArgs textChangedEventArgs)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (o is not MyTextBox textBox) return;
SetByTag(textBox.Tag?.ToString(), textBox.Text);
}
private void SliderChange(object o, bool user)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (o is not MySlider slider) return;
SetByTag(slider.Tag?.ToString(), slider.Value);
}
private void ComboChange(MyComboBox sender, object e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
SetByTag(sender.Tag?.ToString(), sender.SelectedIndex);
}
private void CheckBoxChange(object sender, bool user)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (sender is not MyCheckBox checkBox) return;
SetByTag(checkBox.Tag?.ToString(), checkBox.Checked.GetValueOrDefault());
}
// 切换到全局设置
private void BtnSwitch_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Setup);
}
#region
public void RamType(int type)
{
if (SliderRamCustom is null)
return;
SliderRamCustom.IsEnabled = type == 1;
}
/// <summary>
/// 刷新 UI 上的 RAM 显示。
/// </summary>
public void RefreshRam(bool showAnim)
{
if (LabRamGame is null || LabRamUsed is null ||
ModMain.frmMain.pageCurrent != FormMain.PageType.InstanceSetup ||
ModMain.frmInstanceLeft.pageID != FormMain.PageSubType.VersionSetup)
return;
// 获取内存情况
var ramGame = Math.Round(GetRam(PageInstanceLeft.McInstance), 5);
var phyRam = KernelInterop.GetPhysicalMemoryBytes();
var ramTotal = Math.Round((double)(phyRam.Total / 1024 / 1024 / 1024), 1);
var ramAvailable = Math.Round((double)(phyRam.Available / 1024 / 1024 / 1024), 1);
var ramGameActual = Math.Round(Math.Min(ramGame, ramAvailable), 5);
var ramUsed = Math.Round(ramTotal - ramAvailable, 5);
var ramEmpty = Math.Round(ModBase.MathClamp(ramTotal - ramUsed - ramGame, 0d, 1000d), 1);
// 设置最大可用内存
if (ramTotal <= 1.5d)
SliderRamCustom.MaxValue = (int)Math.Round(Math.Max(Math.Floor((ramTotal - 0.3d) / 0.1d), 1d));
else if (ramTotal <= 8d)
SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 1.5d) / 0.5d) + 12d);
else if (ramTotal <= 16d)
SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 8d) / 1d) + 25d);
else
SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 16d) / 2d) + 33d);
// 设置文本
LabRamGame.Text = $"{Lang.Number(ramGame, "N1")} GiB{(ramGame != ramGameActual ? $" ({Lang.Text("Setup.Launch.Memory.AvailableSuffix", Lang.Number(ramGameActual, "N1"))})" : "")}";
LabRamUsed.Text = $"{Lang.Number(ramUsed, "N1")} GiB";
LabRamTotal.Text = $" / {Lang.Number(ramTotal, "N1")} GiB";
LabRamWarn.Visibility =
ramGame == 1d && !ModJava.IsGameSet64BitJava(PageInstanceLeft.McInstance) && !SystemInfo.Is32BitSystem &&
ModJava.Javas.ExistAnyJava()
? Visibility.Visible
: Visibility.Collapsed;
HintRamTooHigh.Visibility = ramGame / ramTotal > 0.75d ? Visibility.Visible : Visibility.Collapsed;
if (showAnim)
{
// 宽度动画
ModAnimation.AniStart(
new[]
{
ModAnimation.AaGridLengthWidth(ColumnRamUsed, ramUsed - ColumnRamUsed.Width.Value, 800,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
ModAnimation.AaGridLengthWidth(ColumnRamGame, ramGameActual - ColumnRamGame.Width.Value, 800,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)),
ModAnimation.AaGridLengthWidth(ColumnRamEmpty, ramEmpty - ColumnRamEmpty.Width.Value, 800,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong))
}, "VersionSetup Ram Grid");
}
else
{
// 宽度设置
ColumnRamUsed.Width = new GridLength(ramUsed, GridUnitType.Star);
ColumnRamGame.Width = new GridLength(ramGameActual, GridUnitType.Star);
ColumnRamEmpty.Width = new GridLength(ramEmpty, GridUnitType.Star);
}
}
private void RefreshRam()
{
RefreshRam(true);
}
private int ramTextLeft = 2;
private int ramTextRight = 1;
/// <summary>
/// 刷新 UI 上的文本位置。
/// </summary>
private void RefreshRamText()
{
// 获取宽度信息
var rectUsedWidth = RectRamUsed.ActualWidth;
var totalWidth = PanRamDisplay.ActualWidth;
var labGameWidth = LabRamGame.ActualWidth;
var labUsedWidth = LabRamUsed.ActualWidth;
var labTotalWidth = LabRamTotal.ActualWidth;
var labGameTitleWidth = LabRamGameTitle.ActualWidth;
var labUsedTitleWidth = LabRamUsedTitle.ActualWidth;
// 左侧
int left;
if (rectUsedWidth - 30d < labUsedWidth || rectUsedWidth - 30d < labUsedTitleWidth)
// 全写不下了
left = 0;
else if (rectUsedWidth - 25d < labUsedWidth + labTotalWidth)
// 显示不下完整数据
left = 1;
else
// 正常
left = 2;
if (ramTextLeft != left)
{
ramTextLeft = left;
switch (left)
{
case 0:
{
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(LabRamUsed, -LabRamUsed.Opacity, 100),
ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100),
ModAnimation.AaOpacity(LabRamUsedTitle, -LabRamUsedTitle.Opacity, 100)
}, "VersionSetup Ram TextLeft");
break;
}
case 1:
{
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100),
ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100),
ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100)
}, "VersionSetup Ram TextLeft");
break;
}
case 2:
{
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100),
ModAnimation.AaOpacity(LabRamTotal, 1d - LabRamTotal.Opacity, 100),
ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100)
}, "VersionSetup Ram TextLeft");
break;
}
}
}
// 右侧
int right;
if (totalWidth < labGameWidth + 2d + rectUsedWidth || totalWidth < labGameTitleWidth + 2d + rectUsedWidth)
// 挤到最右边
right = 0;
else
// 正常情况
right = 1;
if (right == 0)
{
if (ModAnimation.AniControlEnabled == 0 &&
(ramTextRight != right || ModAnimation.AniIsRun("VersionSetup Ram TextRight")))
{
// 需要动画
ModAnimation.AniStart(
new[]
{
ModAnimation.AaX(LabRamGame, totalWidth - labGameWidth - LabRamGame.Margin.Left, 100,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaX(LabRamGameTitle, totalWidth - labGameTitleWidth - LabRamGameTitle.Margin.Left,
100, ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
}, "VersionSetup Ram TextRight");
}
else
{
// 不需要动画
LabRamGame.Margin = new Thickness(totalWidth - labGameWidth, 3d, 0d, 0d);
LabRamGameTitle.Margin = new Thickness(totalWidth - labGameTitleWidth, 0d, 0d, 5d);
}
}
else if (ModAnimation.AniControlEnabled == 0 &&
(ramTextRight != right || ModAnimation.AniIsRun("VersionSetup Ram TextRight")))
{
// 需要动画
ModAnimation.AniStart(
new[]
{
ModAnimation.AaX(LabRamGame, 2d + rectUsedWidth - LabRamGame.Margin.Left, 100,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaX(LabRamGameTitle, 2d + rectUsedWidth - LabRamGameTitle.Margin.Left, 100,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
}, "VersionSetup Ram TextRight");
}
else
{
// 不需要动画
LabRamGame.Margin = new Thickness(2d + rectUsedWidth, 3d, 0d, 0d);
LabRamGameTitle.Margin = new Thickness(2d + rectUsedWidth, 0d, 0d, 5d);
}
ramTextRight = right;
}
/// <summary>
/// 获取当前设置的 RAM 值。单位为 GB。
/// </summary>
public static double GetRam(McInstance version, bool? is32BitJava = default)
{
var instancePath = version?.PathInstance;
// 跟随全局设置
if (Config.Instance.MemorySolution[instancePath] == 2)
return PageSetupLaunch.GetRam(version, true, is32BitJava);
// ------------------------------------------
// 修改下方代码时需要一并修改 PageSetupLaunch
// ------------------------------------------
// 使用当前实例的设置
var ramGive = default(double);
if (Config.Instance.MemorySolution[instancePath] == 0)
{
// 自动配置
var ramAvailable =
Math.Round((double)(KernelInterop.GetAvailablePhysicalMemoryBytes() / 1024 / 1024 / 1024 * 10)) / 10;
// 确定需求的内存值
double ramMininum; // 无论如何也需要保证的最低限度内存
double ramTarget1; // 估计能勉强带动了的内存
double ramTarget2; // 估计没啥问题了的内存
double ramTarget3; // 安装过多附加组件需要的内存
if (version is not null && !version.IsLoaded)
version.Load();
if (version is not null && version.Modable)
{
// 可安装 Mod 的实例
var modDir = new DirectoryInfo(version.PathIndie + @"mods\");
var modCount = modDir.Exists ? modDir.GetFiles().Length : 0;
ramMininum = 0.5d + modCount / 150d;
ramTarget1 = 1.5d + modCount / 90d;
ramTarget2 = 2.7d + modCount / 50d;
ramTarget3 = 4.5d + modCount / 25d;
}
else if (version is not null && version.Info.HasOptiFine)
{
// OptiFine 实例
ramMininum = 0.5d;
ramTarget1 = 1.5d;
ramTarget2 = 3d;
ramTarget3 = 5d;
}
else
{
// 普通实例
ramMininum = 0.5d;
ramTarget1 = 1.5d;
ramTarget2 = 2.5d;
ramTarget3 = 4d;
}
double ramDelta;
// 预分配内存,阶段一,0 ~ T1,100%
ramDelta = ramTarget1;
ramGive += Math.Min(ramAvailable, ramDelta);
ramAvailable -= ramDelta;
if (ramAvailable >= 0.1d)
{
// 预分配内存,阶段二,T1 ~ T2,70%
ramDelta = ramTarget2 - ramTarget1;
ramGive += Math.Min(ramAvailable * 0.7d, ramDelta);
ramAvailable -= ramDelta / 0.7d;
if (ramAvailable >= 0.1d)
{
// 预分配内存,阶段三,T2 ~ T3,40%
ramDelta = ramTarget3 - ramTarget2;
ramGive += Math.Min(ramAvailable * 0.4d, ramDelta);
ramAvailable -= ramDelta / 0.4d;
if (ramAvailable >= 0.1d)
{
// 预分配内存,阶段四,T3 ~ T3 * 2,15%
ramDelta = ramTarget3;
ramGive += Math.Min(ramAvailable * 0.15d, ramDelta);
ramAvailable -= ramDelta / 0.15d;
}
}
}
// 不低于最低值
ramGive = Math.Round(Math.Max(ramGive, ramMininum), 1);
}
else
{
// 手动配置
var value = Config.Instance.CustomMemorySize[instancePath];
if (value <= 12)
ramGive = value * 0.1d + 0.3d;
else if (value <= 25)
ramGive = (value - 12) * 0.5d + 1.5d;
else if (value <= 33)
ramGive = (value - 25) * 1 + 8;
else
ramGive = (value - 33) * 2 + 16;
}
// 若使用 32 位 Java,则限制为 1G
if (is32BitJava ?? !ModJava.IsGameSet64BitJava(PageInstanceLeft.McInstance))
ramGive = Math.Min(1d, ramGive);
return ramGive;
}
#endregion
#region
// 全局
private int comboServerLoginLast;
private void ComboServerLogin_Changed(object sender, SelectionChangedEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
ServerLogin(ComboServerLoginRequire.SelectedIndex);
if (TextServerAuthServer.IsValidated)
BtnServerAuthLock.IsEnabled = true;
else
BtnServerAuthLock.IsEnabled = false;
if ((ComboServerLoginRequire.SelectedIndex == 2 || ComboServerLoginRequire.SelectedIndex == 3) &&
!TextServerAuthServer.IsValidated)
return;
if (comboServerLoginLast == ComboServerLoginRequire.SelectedIndex)
return;
comboServerLoginLast = ComboServerLoginRequire.SelectedIndex;
Config.InstanceAuth.LoginRequirementSolution[PageInstanceLeft.McInstance.PathInstance] = ComboServerLoginRequire.SelectedIndex;
}
private void TextServerAuthServer_MouseLeave(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(TextServerAuthServer.Text))
return;
if (!(TextServerAuthServer.Text.EndsWithF("/api/yggdrasil/") ||
TextServerAuthServer.Text.EndsWithF("/api/yggdrasil")))
{
if (TextServerAuthServer.Text.EndsWithF("/"))
{
TextServerAuthServer.Text = $"{TextServerAuthServer.Text}api/yggdrasil";
HintService.Hint(Lang.Text("Instance.Setup.Server.AuthServer.AutoFormatted"));
}
else
{
TextServerAuthServer.Text = $"{TextServerAuthServer.Text}/api/yggdrasil";
HintService.Hint(Lang.Text("Instance.Setup.Server.AuthServer.AutoFormatted"));
}
}
if (TextServerAuthServer.Text.EndsWithF("/api/yggdrasil/"))
{
TextServerAuthServer.Text = TextServerAuthServer.Text.BeforeLast("/");
HintService.Hint(Lang.Text("Instance.Setup.Server.AuthServer.AutoFormatted"));
}
comboServerLoginLast = ComboServerLoginRequire.SelectedIndex;
ComboChange(ComboServerLoginRequire, null);
}
public void ServerLogin(int type)
{
LabServerAuthName.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
TextServerAuthName.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
LabServerAuthRegister.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
TextServerAuthRegister.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
LabServerAuthServer.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
TextServerAuthServer.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
BtnServerAuthLittle.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
BtnServerNewProfile.Visibility = type == 2 || type == 3 ? Visibility.Visible : Visibility.Collapsed;
if (type == 0 || type == 1)
BtnServerAuthLock.Visibility = Visibility.Collapsed;
else
BtnServerAuthLock.Visibility = Visibility.Visible;
if (Config.InstanceAuth.AuthLocked[PageInstanceLeft.McInstance.PathInstance])
{
HintServerLoginLock.Visibility = Visibility.Visible;
ComboServerLoginRequire.IsEnabled = false;
TextServerAuthServer.IsEnabled = false;
TextServerAuthName.IsEnabled = false;
TextServerAuthRegister.IsEnabled = false;
BtnServerAuthLittle.IsEnabled = false;
}
else
{
HintServerLoginLock.Visibility = Visibility.Collapsed;
ComboServerLoginRequire.IsEnabled = true;
TextServerAuthServer.IsEnabled = true;
TextServerAuthName.IsEnabled = true;
TextServerAuthRegister.IsEnabled = true;
BtnServerAuthLittle.IsEnabled = true;
}
CardServer.TriggerForceResize();
// 避免正版验证和离线验证出现此提示
if (type != 2 && type != 3)
{
LabServerAuthServerSecurity.Visibility = Visibility.Collapsed;
LabServerAuthServerSecurityCL.Visibility = Visibility.Collapsed;
LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed;
}
// 如果开头为 http:// 给予警告
else if (TextServerAuthServer.Text.StartsWithF("https://"))
{
LabServerAuthServerSecurity.Visibility = Visibility.Collapsed;
LabServerAuthServerSecurityVerify.Visibility = Visibility.Visible;
LabServerAuthServerSecurityCL.Visibility = Visibility.Visible;
}
else if (TextServerAuthServer.Text.StartsWithF("http://"))
{
LabServerAuthServerSecurity.Visibility = Visibility.Visible;
LabServerAuthServerSecurityCL.Visibility = Visibility.Visible;
LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed;
}
else
{
LabServerAuthServerSecurity.Visibility = Visibility.Collapsed;
LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed;
LabServerAuthServerSecurityCL.Visibility = Visibility.Collapsed;
}
}
// LittleSkin
private void BtnServerAuthLittle_Click(object sender, MouseButtonEventArgs e)
{
if (!string.IsNullOrEmpty(TextServerAuthServer.Text) &&
TextServerAuthServer.Text != "https://littleskin.cn/api/yggdrasil" && ModMain.MyMsgBox(
Lang.Text("Instance.Setup.Server.LittleSkin.Override.Message"),
Lang.Text("Instance.Setup.Server.LittleSkin.Override.Title"), Lang.Text("Instance.Setup.Server.LittleSkin.Override.Continue"), Lang.Text("Common.Action.Cancel")) == 2)
return;
TextServerAuthServer.Text = "https://littleskin.cn/api/yggdrasil";
TextServerAuthRegister.Text = "https://littleskin.cn/auth/register";
TextServerAuthName.Text = Lang.Text("Instance.Setup.Server.LittleSkin.Name");
}
// 锁定设置
private void BtnServerAuthLock_Click(object sender, MouseButtonEventArgs e)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Setup.Server.LockLoginMethod.Message"),
Lang.Text("Instance.Setup.Server.LockLoginMethod.Title"), Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 1)
{
Config.InstanceAuth.AuthLocked[PageInstanceLeft.McInstance.PathInstance] = true;
Reload();
}
}
// 跳转新建档案
private void BtnServerNewProfile_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
PageLoginAuth.draggedAuthServer = TextServerAuthServer.Text;
ModBase.RunInNewThread(() =>
{
Thread.Sleep(150);
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth));
});
}
private static void TextServerEnter_Change(object sender, TextChangedEventArgs e)
{
if (sender is MyTextBox textBox) textBox.Text = textBox.Text.Replace("", ":");
}
#endregion
#region Java
// 刷新 Java 下拉框显示
public void RefreshJavaComboBox()
{
if (ComboArgumentJava is null)
return;
// 获取实例的 Java 偏好(已兼容新旧格式)
var preference = ModJava.GetInstanceJavaPreference(PageInstanceLeft.McInstance);
// === 1. 初始化固定选项(使用类型安全的 Tag) ===
ComboArgumentJava.Items.Clear();
// 选项 0: 跟随全局设置
ComboArgumentJava.Items.Add(new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.FollowGlobal"),
Tag = new UseGlobalPreference()
});
// 选项 1: 自动选择
ComboArgumentJava.Items.Add(new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.AutoSelect"),
Tag = new AutoSelect() // Nothing 表示自动选择
});
// 选项 2: 相对路径选项
MyComboBoxItem relativePathItem;
if (preference is UseRelativePath)
{
var relPref = (UseRelativePath)preference;
var absPath = Path.GetFullPath(Path.Combine(Basics.ExecutableDirectory, relPref.RelativePath));
var javaEntry = ModJava.Javas.Get(absPath);
if (Files.IsPathWithinDirectory(absPath, Basics.ExecutableDirectory) && javaEntry is not null &&
javaEntry.IsEnabled)
// 有效路径:显示具体 Java 信息
relativePathItem = new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.SelectRelative.WithJava", javaEntry.ToString()),
Tag = new UseRelativePath(relPref.RelativePath),
ToolTip = Lang.Text("Instance.Setup.Options.Java.RelativePathToolTip", relPref.RelativePath, absPath)
};
else
// 无效路径:提示用户重新选择
relativePathItem = new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.SelectRelative.Invalid"),
Tag = new UseRelativePath(relPref.RelativePath),
ToolTip = Lang.Text("Instance.Setup.Options.Java.InvalidPathToolTip", absPath)
};
}
else
{
// 未配置相对路径:使用默认模板
relativePathItem = new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.SelectRelative"),
Tag = new UseRelativePath(@"jre\bin\java.exe"),
ToolTip = Lang.Text("Instance.Setup.Options.Java.SelectRelativeToolTip")
};
}
ComboArgumentJava.Items.Add(relativePathItem);
// === 2. 添加所有可用 Java 运行时 ===
MyComboBoxItem selectedItem = null;
try
{
foreach (var curJava in ModJava.Javas.GetSortedJavaList())
{
var item = new MyComboBoxItem
{
Content = curJava.ToString(),
ToolTip =
Lang.Text("Instance.Setup.Options.Java.Details.ToolTip", curJava.Installation.JavaExePath, curJava.Installation.Version, curJava.Source),
Tag = curJava
};
ToolTipService.SetInitialShowDelay(item, 300);
ToolTipService.SetBetweenShowDelay(item, 100);
ComboArgumentJava.Items.Add(item);
}
}
catch (Exception ex)
{
Config.Instance.SelectedJava[PageInstanceLeft.McInstance.PathInstance] = "使用全局设置";
ModBase.Log(
ex,
"更新实例设置 Java 下拉框失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Setup.Error.OperationFailed"));
ComboArgumentJava.Items.Clear();
ComboArgumentJava.Items.Add(new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.LoadFailed"),
IsEnabled = false
});
ComboArgumentJava.SelectedIndex = 0;
RefreshRam(true);
return;
}
// === 3. 根据当前偏好设置选中项(优先使用新格式 preference ===
if (preference is null)
{
// 自动选择
selectedItem = ComboArgumentJava.Items[1] as MyComboBoxItem;
}
else if (preference is UseGlobalPreference)
{
selectedItem = ComboArgumentJava.Items[0] as MyComboBoxItem;
}
else if (preference is UseRelativePath)
{
selectedItem = ComboArgumentJava.Items[2] as MyComboBoxItem;
}
else if (preference is ExistingJava)
{
var existPref = (ExistingJava)preference;
// 在 Java 列表中查找匹配项(从索引 3 开始)
for (int i = 3, loopTo = ComboArgumentJava.Items.Count - 1; i <= loopTo; i++)
{
var item = ComboArgumentJava.Items[i] as MyComboBoxItem;
if (item is not null && item.Tag is JavaEntry)
{
var javaEntry = (JavaEntry)item.Tag;
if (string.Equals(javaEntry.Installation.JavaExePath, existPref.JavaExePath,
StringComparison.OrdinalIgnoreCase))
{
selectedItem = item;
break;
}
}
}
}
// 降级处理:无匹配项时回退到自动选择
if (selectedItem is null && ComboArgumentJava.Items.Count > 1)
selectedItem = ComboArgumentJava.Items[1] as MyComboBoxItem;
// 设置选中项
if (selectedItem is not null) ComboArgumentJava.SelectedItem = selectedItem;
// === 4. 无可用 Java 时的降级处理 ===
if (!ModJava.Javas.ExistAnyJava() && ComboArgumentJava.Items.Count <= 3)
{
ComboArgumentJava.Items.Clear();
var noJavaItem = new MyComboBoxItem
{
Content = Lang.Text("Instance.Setup.Options.Java.NoRuntime"),
ToolTip = Lang.Text("Instance.Setup.Options.Java.NoRuntime.ToolTip"),
IsEnabled = false
};
ComboArgumentJava.Items.Add(noJavaItem);
ComboArgumentJava.SelectedItem = noJavaItem;
}
// === 5. 刷新关联控件 ===
RefreshRam(true);
}
// 阻止在无效状态下展开下拉框
private void ComboArgumentJava_DropDownOpened(object? sender, EventArgs e)
{
if (ComboArgumentJava.SelectedItem is null)
{
ComboArgumentJava.IsDropDownOpen = false;
return;
}
var firstItem = ComboArgumentJava.Items[0] as MyComboBoxItem;
if (firstItem is not null &&
((string)firstItem.Content == Lang.Text("Instance.Setup.Options.Java.NoRuntime") ||
(string)firstItem.Content == Lang.Text("Instance.Setup.Options.Java.LoadFailed")))
ComboArgumentJava.IsDropDownOpen = false;
}
// 下拉框选择更改处理(保存新格式配置)
private void JavaSelectionUpdate(object sender, SelectionChangedEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (ComboArgumentJava.SelectedItem is null)
return;
var selectedItem = ComboArgumentJava.SelectedItem as MyComboBoxItem;
if (selectedItem is null || (selectedItem.Tag is null &&
(string)selectedItem.Content != Lang.Text("Instance.Setup.Options.Java.AutoSelect")))
return;
JavaPreference preference = default;
var logMessage = "";
// 根据 Tag 类型生成偏好对象
if (selectedItem.Tag is null or AutoSelect)
{
// 自动选择:存储空字符串
preference = new AutoSelect();
logMessage = "[Java] 修改实例 Java 选择设置:自动选择";
}
else if (selectedItem.Tag is UseGlobalPreference)
{
preference = new UseGlobalPreference();
logMessage = "[Java] 修改实例 Java 选择设置:跟随全局设置";
}
else if (selectedItem.Tag is UseRelativePath)
{
// 相对路径:需要用户选择实际文件
var ret = SystemDialogs.SelectFile(Lang.Text("Setup.Java.SelectFile.Filter"), Lang.Text("Setup.Java.SelectFile.Title"), Basics.ExecutableDirectory);
if (string.IsNullOrWhiteSpace(ret))
// 用户取消,不保存配置,保持原选择
return;
ret = Path.GetFullPath(ret);
var relativePath = Path.GetRelativePath(Basics.ExecutableDirectory, ret);
// 验证路径是否在启动器目录内
if (!Files.IsPathWithinDirectory(relativePath, Basics.ExecutableDirectory))
{
HintService.Hint(Lang.Text("Instance.Setup.Options.Java.PathOutOfRange"), HintType.Error);
return;
}
preference = new UseRelativePath(relativePath);
logMessage = $"[Java] 修改实例 Java 选择设置:相对路径 | {relativePath}";
}
else if (selectedItem.Tag is JavaEntry)
{
var javaEntry = (JavaEntry)selectedItem.Tag;
preference = new ExistingJava(javaEntry.Installation.JavaExePath);
logMessage = $"[Java] 修改实例 Java 选择设置:{javaEntry}";
}
// 保存配置
var json = JsonSerializer.Serialize(preference, JsonCompat.SerializerOptions);
Config.Instance.SelectedJava[PageInstanceLeft.McInstance.PathInstance] = json;
ModBase.Log(logMessage);
RefreshRam(true);
}
#endregion
#region
// 版本隔离警告
private bool isReverting;
private void ComboArgumentIndieV2_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
if (isReverting)
return;
if (ModMain.MyMsgBox(
Lang.Text("Instance.Setup.Options.InstanceIsolation.Message"),
Lang.Text("Common.Dialog.Warning"), Lang.Text("Setup.Launch.Advanced.Renderer.Warning.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 2)
{
isReverting = true;
ComboArgumentIndieV2.SelectedItem = e.RemovedItems[0];
isReverting = false;
}
else
{
bool newValue = ComboArgumentIndieV2.SelectedIndex == 0;
Config.Instance.IndieV2[PageInstanceLeft.McInstance.PathInstance] = newValue;
}
}
// 游戏窗口
private void CheckArgumentTitleEmpty_Change(object sender, bool e)
{
TextArgumentTitle.HintText = CheckArgumentTitleEmpty.Checked == true ? Lang.Text("Common.Option.Default") : Lang.Text("Instance.Setup.FollowGlobal");
CheckBoxChange(sender,e);
}
private void TextArgumentTitle_TextChanged(object sender, TextChangedEventArgs e)
{
CheckArgumentTitleEmpty.Visibility = TextArgumentTitle.Text.Length > 0 ? Visibility.Collapsed : Visibility.Visible;
TextBoxChange(sender,e);
}
#endregion
#region
private void TextAdvanceRun_TextChanged(object sender, TextChangedEventArgs e)
{
CheckAdvanceRunWait.Visibility = string.IsNullOrEmpty(TextAdvanceRun.Text) ? Visibility.Collapsed : Visibility.Visible;
TextBoxChange(sender,e);
}
private void ComboAdvanceRenderer_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
var args = e; // 转换事件参数
if (!States.Hint.Renderer && ComboAdvanceRenderer.SelectedIndex != 0)
{
if (ModMain.MyMsgBox(Lang.Text("Setup.Launch.Advanced.Renderer.Warning.Message"),
Lang.Text("Common.Dialog.Warning"),
Lang.Text("Setup.Launch.Advanced.Renderer.Warning.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 2)
{
ComboAdvanceRenderer.SelectedItem = args.RemovedItems[0];
}
else
{
ComboChange(ComboAdvanceRenderer, e);
States.Hint.Renderer = true;
}
}
else
{
ComboChange(ComboAdvanceRenderer, e);
}
}
private void CheckUseDebugLog4j2Config_CheckChanged(object sender, bool e)
{
if (ModAnimation.AniControlEnabled != 0)
return;
var checkBox = sender as MyCheckBox;
if (checkBox is null) return;
if (checkBox.Checked.GetValueOrDefault() && !States.Hint.DebugLog4j2Config)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Setup.Advanced.UseDebugLog4j.Message"),
Lang.Text("Common.Dialog.Warning"), Lang.Text("Setup.Launch.Advanced.Renderer.Warning.Confirm"), Lang.Text("Common.Action.Cancel"), isWarn: true) == 2)
{
checkBox.Checked = false;
}
else
{
CheckBoxChange(sender, e);
States.Hint.DebugLog4j2Config = true;
}
}
else
{
CheckBoxChange(sender, e);
}
}
#endregion
}
@@ -0,0 +1,88 @@
<local:MyCard x:Class="PCL.ServerCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL;assembly="
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
Margin="0,8">
<Grid
x:Name="PanBack" Height="44"
RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}"
SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="6" />
<ColumnDefinition Width="34" />
<ColumnDefinition Width="7" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="350" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="17" />
<RowDefinition Height="18" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<!-- Icon -->
<Border Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" IsHitTestVisible="False" SnapsToDevicePixels="True"
UseLayoutRounding="True"
HorizontalAlignment="Right" VerticalAlignment="Center" Width="34" Height="34">
<Border.Clip>
<RectangleGeometry Rect="0,0,34,34" RadiusX="6" RadiusY="6" />
</Border.Clip>
<Image x:Name="ServerIcon" RenderOptions.BitmapScalingMode="HighQuality" />
</Border>
<!-- 标题 -->
<TextBlock Grid.Column="3" Grid.Row="1" VerticalAlignment="Bottom" x:Name="ServerName"
TextTrimming="CharacterEllipsis" FontSize="14"
HorizontalAlignment="Left" ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}"
ToolTipService.Placement="Top" MaxWidth="180" />
<!-- ping 值与玩家人数 -->
<StackPanel Grid.Row="2" Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Bottom">
<ContentPresenter x:Name="Signal" Content="{Binding SelectedIcon.XamlContent}"
Width="20" Height="20" />
<TextBlock x:Name="ServerPlayer" HorizontalAlignment="Left"
TextTrimming="CharacterEllipsis" FontSize="12"
Foreground="{DynamicResource ColorBrushGray3}" Opacity="0.4" VerticalAlignment="Center" />
</StackPanel>
<!-- MotD -->
<controls:MotdRenderer x:Name="MotdRenderer" Grid.Row="1" Grid.RowSpan="2" Grid.Column="4" Margin="0,0,3,1"
IsHitTestVisible="False" HorizontalAlignment="Center" Width="350" Height="34" />
<TextBlock Grid.Row="1" x:Name="ServerMotD" Grid.RowSpan="2" Grid.Column="4" VerticalAlignment="Center"
Margin="0,0,3,1"
TextTrimming="CharacterEllipsis" FontSize="12" Foreground="{DynamicResource ColorBrushGray4}"
IsHitTestVisible="False" HorizontalAlignment="Center" Visibility="Visible" />
<!--<Canvas x:Name="MotdCanvas" Grid.Row="1" Grid.RowSpan="2" Grid.Column="4" Margin="0,0,3,1"
IsHitTestVisible="False" HorizontalAlignment="Center" Width="300" Height="34"/>-->
<StackPanel Grid.Row="1" Grid.Column="5" Grid.RowSpan="2" Orientation="Horizontal" VerticalAlignment="Center"
Margin="0,0,4,0">
<local:MyIconButton
Width="24"
SvgIcon="lucide/play"
Click="BtnConnect_Click" />
<local:MyIconButton
x:Name="BtnSetting"
Click="BtnSkin_Click"
Width="24"
LogoScale="1.2"
SvgIcon="lucide/settings">
<local:MyIconButton.ContextMenu>
<ContextMenu>
<local:MyMenuItem Click="BtnRefresh_Click" Header="{DynamicResource Common.Action.Refresh}"
Icon="M849.493333 661.76h-193.28a42.666667 42.666667 0 0 0 0 85.333333h102.4A341.333333 341.333333 0 0 1 170.666667 512a42.666667 42.666667 0 0 0-85.333334 0 426.666667 426.666667 0 0 0 720.213334 308.48V896a42.666667 42.666667 0 0 0 85.333333 0v-192a42.666667 42.666667 0 0 0-41.386667-42.24zM640 512a128 128 0 1 0-128 128 128 128 0 0 0 128-128z m-170.666667 0a42.666667 42.666667 0 1 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667-42.666667z m42.666667-426.666667a426.666667 426.666667 0 0 0-293.546667 118.186667V128a42.666667 42.666667 0 0 0-85.333333 0v192a42.666667 42.666667 0 0 0 42.666667 42.666667h192a42.666667 42.666667 0 0 0 0-85.333334h-102.4A341.333333 341.333333 0 0 1 853.333333 512a42.666667 42.666667 0 0 0 85.333334 0A426.666667 426.666667 0 0 0 512 85.333333z" />
<local:MyMenuItem Click="BtnCopy_Click"
Header="{DynamicResource Instance.Server.Card.CopyAddress}"
Icon="M931.882 131.882l-103.764-103.764A96 96 0 0 0 760.236 0H416c-53.02 0-96 42.98-96 96v96H160c-53.02 0-96 42.98-96 96v640c0 53.02 42.98 96 96 96h448c53.02 0 96-42.98 96-96v-96h160c53.02 0 96-42.98 96-96V199.764a96 96 0 0 0-28.118-67.882zM596 928H172a12 12 0 0 1-12-12V300a12 12 0 0 1 12-12h148v448c0 53.02 42.98 96 96 96h192v84a12 12 0 0 1-12 12z m256-192H428a12 12 0 0 1-12-12V108a12 12 0 0 1 12-12h212v176c0 26.51 21.49 48 48 48h176v404a12 12 0 0 1-12 12z m12-512h-128V96h19.264c3.182 0 6.234 1.264 8.486 3.514l96.736 96.736a12 12 0 0 1 3.514 8.486V224z" />
<local:MyMenuItem Click="BtnEdit_Click" Header="{DynamicResource Instance.Server.Card.Edit}"
Icon="M934.4 981.333333H89.6A46.08 46.08 0 0 1 42.666667 934.485333V156.672a46.08 46.08 0 0 1 46.933333-46.848h367.658667c26.581333 0 46.933333 20.309333 46.933333 46.848a46.08 46.08 0 0 1-46.933333 46.848H136.533333v684.117333h750.933334v-371.712a46.08 46.08 0 0 1 46.933333-46.890666c26.581333 0 46.933333 20.309333 46.933333 46.848v418.56c0 26.581333-21.888 46.890667-46.933333 46.890666z M330.538667 739.242667c-12.544 0-25.045333-4.693333-32.853334-14.08-9.386667-9.344-14.08-20.266667-14.08-32.768v-187.434667c0-12.501333 4.693333-24.96 14.08-32.768l416.128-415.488c7.808-9.386667 20.352-14.037333 32.853334-14.037333 12.501333 0 25.045333 4.693333 32.853333 14.08l187.733333 187.392c18.773333 18.773333 18.773333 48.426667 0 65.621333l-416.128 415.445333c-9.386667 9.386667-20.352 14.08-32.853333 14.08h-187.733333z m46.933333-215.552v120.32h120.448l369.194667-367.061334L746.666667 155.136l-369.194667 368.597333z" />
<Separator />
<local:MyMenuItem Click="BtnRemove_Click"
Header="{DynamicResource Instance.Server.Card.Remove}"
Icon="M683.514192 1023.999519H340.485808A154.527713 154.527713 0 0 1 186.336081 884.785983L129.317406 325.755552a39.082648 39.082648 0 0 1 77.762111-7.937715l57.053038 559.030431a76.559427 76.559427 0 0 0 76.376161 68.976791h343.005476a76.559427 76.559427 0 0 0 76.376161-68.976791l57.053038-559.030431a39.082648 39.082648 0 1 1 77.762112 7.937715l-57.053039 559.030431A154.516259 154.516259 0 0 1 683.514192 1023.999519zM888.85244 191.513109h-753.681972a39.092957 39.092957 0 0 1 0-78.17446h753.681972a39.092957 39.092957 0 0 1 0 78.17446z M409.199153 704.887365a39.092957 39.092957 0 0 1-39.081502-39.081503V451.991555a39.092957 39.092957 0 0 1 78.174459 0v213.814307a39.092957 39.092957 0 0 1-39.092957 39.081503zM614.789392 704.887365a39.092957 39.092957 0 0 1-39.081502-39.081503V451.991555a39.092957 39.092957 0 0 1 78.174459 0v213.814307a39.092957 39.092957 0 0 1-39.092957 39.081503zM667.844938 171.960904a39.081503 39.081503 0 0 1-39.127319-39.081503 54.773666 54.773666 0 0 0-54.716395-54.716395h-124.048264a54.773666 54.773666 0 0 0-54.716395 54.716395 39.081503 39.081503 0 1 1-78.163006 0 133.028304 133.028304 0 0 1 132.867947-132.867947H574.04704a133.028304 133.028304 0 0 1 132.867947 132.867947 39.070049 39.070049 0 0 1-39.070049 39.081503z" />
</ContextMenu>
</local:MyIconButton.ContextMenu>
</local:MyIconButton>
</StackPanel>
</Grid>
</local:MyCard>
@@ -0,0 +1,238 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using PCL.Core.UI;
using PCL.Core.UI.Theme;
using PCL.Core.App.Localization;
namespace PCL;
public partial class ServerCard
{
private readonly IconManager _manager;
public MinecraftServerInfo server;
public ServerCard()
{
InitializeComponent();
DataContext = new IconManager();
// 示例:可在代码中切换图标
_manager = DataContext as IconManager;
_manager.AddIconFromXaml("signal_1",
"<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"20\" Height=\"20\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"234.666667\" Canvas.Top=\"610.56\" Width=\"80.853333\" Height=\"127.04\" Fill=\"#ff00ff21\"/></Canvas><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"353.066667\" Canvas.Top=\"541.226667\" Width=\"80.853333\" Height=\"196.373333\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"471.445333\" Canvas.Top=\"460.373333\" Width=\"80.896\" Height=\"277.226667\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"589.866667\" Canvas.Top=\"379.52\" Width=\"80.853333\" Height=\"358.08\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"708.266667\" Canvas.Top=\"298.666667\" Width=\"80.853333\" Height=\"438.933333\" Fill=\"#ff888888\"/></Canvas></Canvas></Viewbox>");
_manager.AddIconFromXaml("signal_2",
"<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"20\" Height=\"20\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"234.666667\" Canvas.Top=\"610.56\" Width=\"80.853333\" Height=\"127.04\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"353.066667\" Canvas.Top=\"541.226667\" Width=\"80.853333\" Height=\"196.373333\" Fill=\"#ff00ff21\"/></Canvas><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"471.445333\" Canvas.Top=\"460.373333\" Width=\"80.896\" Height=\"277.226667\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"589.866667\" Canvas.Top=\"379.52\" Width=\"80.853333\" Height=\"358.08\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"708.266667\" Canvas.Top=\"298.666667\" Width=\"80.853333\" Height=\"438.933333\" Fill=\"#ff888888\"/></Canvas></Canvas></Viewbox>");
_manager.AddIconFromXaml("signal_3",
"<Viewbox Width=\"20\" Height=\"20\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"234.666667\" Canvas.Top=\"610.56\" Width=\"80.853333\" Height=\"127.04\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"353.066667\" Canvas.Top=\"541.226667\" Width=\"80.853333\" Height=\"196.373333\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"471.445333\" Canvas.Top=\"460.373333\" Width=\"80.896\" Height=\"277.226667\" Fill=\"#ff00ff21\"/></Canvas><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"589.866667\" Canvas.Top=\"379.52\" Width=\"80.853333\" Height=\"358.08\" Fill=\"#ff888888\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"708.266667\" Canvas.Top=\"298.666667\" Width=\"80.853333\" Height=\"438.933333\" Fill=\"#ff888888\"/></Canvas></Canvas></Viewbox>");
_manager.AddIconFromXaml("signal_4",
"<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"20\" Height=\"20\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"234.666667\" Canvas.Top=\"610.56\" Width=\"80.853333\" Height=\"127.04\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"353.066667\" Canvas.Top=\"541.226667\" Width=\"80.853333\" Height=\"196.373333\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"471.445333\" Canvas.Top=\"460.373333\" Width=\"80.896\" Height=\"277.226667\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"589.866667\" Canvas.Top=\"379.52\" Width=\"80.853333\" Height=\"358.08\" Fill=\"#ff00ff21\"/></Canvas><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"708.266667\" Canvas.Top=\"298.666667\" Width=\"80.853333\" Height=\"438.933333\" Fill=\"#ff888888\"/></Canvas></Canvas></Viewbox>");
_manager.AddIconFromXaml("signal_5",
"<Viewbox Width=\"20\" Height=\"20\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Canvas UseLayoutRounding=\"False\"><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"234.666667\" Canvas.Top=\"610.56\" Width=\"80.853333\" Height=\"127.04\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"353.066667\" Canvas.Top=\"541.226667\" Width=\"80.853333\" Height=\"196.373333\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"471.445333\" Canvas.Top=\"460.373333\" Width=\"80.896\" Height=\"277.226667\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"589.866667\" Canvas.Top=\"379.52\" Width=\"80.853333\" Height=\"358.08\" Fill=\"#ff00ff21\"/><Rectangle RadiusX=\"0.0\" RadiusY=\"0.0\" Canvas.Left=\"708.266667\" Canvas.Top=\"298.666667\" Width=\"80.853333\" Height=\"438.933333\" Fill=\"#ff00ff21\"/></Canvas></Canvas></Viewbox>");
_manager.AddIconFromXaml("signal_offline",
"<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"14\" Height=\"14\" Margin=\"3\"><Canvas UseLayoutRounding=\"False\" Width=\"1280.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1280.0,1024.0\"/></Canvas.Clip><Path Fill=\"#ff000000\"><Path.Data><PathGeometry Figures=\"M 317.63 349.235 l -67.951 -67.951 l -67.95 67.95 c -18.964 18.964 -48.988 18.964 -67.951 0 c -18.963 -18.962 -18.963 -48.987 0 -67.95 l 67.95 -67.95 l -66.37 -67.951 c -18.963 -18.963 -18.963 -48.988 0 -67.95 c 18.963 -18.964 48.988 -18.964 67.95 0 l 67.951 67.95 l 67.95 -67.95 c 18.964 -18.964 48.989 -18.964 67.951 0 c 18.963 18.962 18.963 48.987 0 67.95 l -67.95 67.95 l 67.95 67.951 c 18.963 18.963 18.963 48.988 0 67.95 c -9.481 9.482 -20.543 14.223 -33.185 14.223 c -14.222 0 -26.864 -6.321 -36.345 -14.222 z M 216.494 752.198 h -48.988 c -26.864 0 -48.987 26.864 -48.987 60.049 v 120.099 c 0 33.185 22.123 60.05 48.987 60.05 h 48.988 c 26.864 0 48.987 -26.865 48.987 -60.05 v -120.1 c 0 -33.184 -22.123 -60.048 -48.987 -60.048 z M 516.74 512 h -48.988 c -26.864 0 -48.988 26.864 -48.988 60.05 v 360.296 c 0 33.185 22.124 60.05 48.988 60.05 h 48.988 c 26.864 0 48.987 -26.865 48.987 -60.05 V 572.049 c 0 -33.185 -22.123 -60.049 -48.987 -60.049 z m 300.247 -240.198 H 768 c -26.864 0 -48.988 26.865 -48.988 60.05 v 600.494 c 0 33.185 22.124 60.05 48.988 60.05 h 48.988 c 26.864 0 48.987 -26.865 48.987 -60.05 V 331.852 c 0 -33.185 -22.123 -60.05 -48.987 -60.05 z m 300.247 -240.197 h -48.988 c -26.864 0 -48.988 26.864 -48.988 60.05 v 840.69 c 0 33.186 22.124 60.05 48.988 60.05 h 48.988 c 26.864 0 48.987 -26.864 48.987 -60.05 V 91.656 c -1.58 -33.186 -22.123 -60.05 -48.987 -60.05 z\" FillRule=\"Nonzero\"/></Path.Data></Path></Canvas></Viewbox>");
_manager.AddIconFromXaml("loading",
"<Viewbox xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"20\" Height=\"20\"><Canvas UseLayoutRounding=\"False\" Width=\"1024.0\" Height=\"1024.0\"><Canvas.Clip><RectangleGeometry Rect=\"0.0,0.0,1024.0,1024.0\"/></Canvas.Clip><Path Fill=\"#ff000000\"><Path.Data><PathGeometry Figures=\"M 256 490.667 a 64 64 0 1 1 -128 0 a 64 64 0 0 1 128 0 z m -42.6667 0 a 21.3333 21.3333 0 1 0 -42.6667 0 a 21.3333 21.3333 0 0 0 42.6667 0 z m 384 0 a 106.667 106.667 0 1 1 -213.376 -0.042667 A 106.667 106.667 0 0 1 597.333 490.667 z m -42.6667 0 a 64 64 0 1 0 -128.043 0.042666 A 64 64 0 0 0 554.667 490.667 z m 298.667 0 a 64 64 0 1 1 -128 0 a 64 64 0 0 1 128 0 z m -42.6667 0 a 21.3333 21.3333 0 1 0 -42.6667 0 a 21.3333 21.3333 0 0 0 42.6667 0 z\" FillRule=\"Nonzero\"/></Path.Data></Path></Canvas></Viewbox>");
}
public event EventHandler? RemoveServer;
public event EventHandler? EditServer;
private void BtnSkin_Click(object sender, EventArgs eventArgs)
{
BtnSetting.ContextMenu.IsOpen = true;
}
/// <summary>
/// 初始化服务器卡片
/// </summary>
public void UpdateServerInfo(MinecraftServerInfo serverInfo)
{
server = serverInfo;
ModBase.RunInUi(() => UpdateServerUi());
}
/// <summary>
/// 更新服务器UI
/// </summary>
private async void UpdateServerUi()
{
if (server is null)
return;
// 更新服务器名称
ServerName.Text = server.Name;
await ImageLoaderHelper.SetServerLogoAsync(server.Icon, ServerIcon);
if (server.Status == ServerStatus.Online)
{
_manager.SetSelectedIconByName(GetSignalIcon(server.Ping));
Signal.ToolTip = $"{server.Ping}ms";
ToolTipService.SetInitialShowDelay(Signal, 0);
ToolTipService.SetBetweenShowDelay(Signal, 50);
ToolTipService.SetPlacement(Signal, PlacementMode.Top);
if (server.PlayerCount != default && server.MaxPlayers != default)
ServerPlayer.Text = $"{server.PlayerCount} / {server.MaxPlayers}";
else
ServerPlayer.Text = "???";
ServerMotD.Visibility = Visibility.Collapsed;
MotdRenderer.RenderMotd(server.Description, ThemeService.IsDarkMode, 2);
MotdRenderer.RenderCanvas();
}
else if (server.Status == ServerStatus.Pinging)
{
_manager.SetSelectedIconByName("loading");
MotdRenderer.ClearCanvas();
ServerPlayer.Text = Lang.Text("Instance.Server.Card.Connecting");
ServerMotD.Text = Lang.Text("Instance.Server.Card.ConnectingDots");
ServerMotD.Visibility = Visibility.Visible;
}
else if (server.Status == ServerStatus.Offline)
{
_manager.SetSelectedIconByName("signal_offline");
MotdRenderer.ClearCanvas();
ServerPlayer.Text = Lang.Text("Instance.Server.Card.Offline");
ServerMotD.Text = Lang.Text("Instance.Server.Card.ServerOffline");
ServerMotD.Visibility = Visibility.Visible;
}
}
private string GetSignalIcon(int ping)
{
switch (ping)
{
case var @case when 0 <= @case && @case <= 99:
{
return "signal_5"; // 5 条信号
}
case var case1 when 100 <= case1 && case1 <= 299:
{
return "signal_4"; // 4 条信号
}
case var case2 when 300 <= case2 && case2 <= 599:
{
return "signal_3"; // 3 条信号
}
case var case3 when 600 <= case3 && case3 <= 999:
{
return "signal_2"; // 2 条信号
}
default:
{
return "signal_1"; // 1 条信号
}
}
}
/// <summary>
/// 刷新服务器状态
/// </summary>
public async Task RefreshServerStatusAsync(bool withHint, CancellationToken token = default)
{
if (withHint) HintService.Hint(Lang.Text("Instance.Server.Card.RefreshingStatus", server.Name));
server.Status = ServerStatus.Pinging;
await Dispatcher.InvokeAsync(() => UpdateServerUi());
var serverInfo = await PageInstanceServer.PingServerAsync(server, token);
UpdateServerInfo(serverInfo);
}
/// <summary>
/// 连接到服务器
/// </summary>
private void BtnConnect_Click(object sender, EventArgs e)
{
try
{
var launchOptions = new ModLaunch.McLaunchOptions
{
ServerIp = server.Address,
instance = PageInstanceLeft.McInstance
};
ModLaunch.McLaunchStart(launchOptions);
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
HintService.Hint(Lang.Text("Instance.Server.Card.ConnectingTo", server.Name));
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Server.Card.LaunchFailed"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Instance.Server.Card.LaunchFailed"));
HintService.Hint(Lang.Text("Instance.Server.Card.LaunchFailedMsg", ex.Message), HintType.Error);
}
}
/// <summary>
/// 复制服务器地址
/// </summary>
private void BtnCopy_Click(object sender, RoutedEventArgs e)
{
try
{
Clipboard.SetText(server.Address);
HintService.Hint(Lang.Text("Instance.Server.Card.AddressCopied", server.Address), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(ex, Lang.Text("Instance.Server.Card.CopyAddressFailed"));
HintService.Hint(Lang.Text("Instance.Server.Card.CopyAddressFailed"), HintType.Error);
}
}
/// <summary>
/// 刷新服务器状态
/// </summary>
private async void BtnRefresh_Click(object sender, RoutedEventArgs e)
{
await Task.Run(async () => await RefreshServerStatusAsync(true));
}
/// <summary>
/// 编辑服务器信息
/// </summary>
private void BtnEdit_Click(object sender, RoutedEventArgs e)
{
try
{
// Get server information
var result = PageInstanceServer.GetServerInfo(server);
if (!result.Success) return;
EditServer?.Invoke(this, new ResultEventArgs(result.Name, result.Address));
}
// Update server object
// _server.Name = result.Name
// _server.Address = result.Address
catch (Exception ex)
{
HintService.Hint(Lang.Text("Instance.Server.Card.EditFailed", ex.Message), HintType.Error);
}
}
private void BtnRemove_Click(object sender, RoutedEventArgs e)
{
if (ModMain.MyMsgBox(
Lang.Text("Instance.Server.Card.RemoveConfirmMessage", server.Name, server.Address),
Lang.Text("Instance.Server.Card.RemoveConfirmTitle"), Lang.Text("Common.Action.Confirm"),
Lang.Text("Common.Action.Cancel")
) == 1) RemoveServer?.Invoke(this, EventArgs.Empty);
}
public class ResultEventArgs : EventArgs
{
public ResultEventArgs(string param1, string param2)
{
Param1 = param1;
Param2 = param2;
}
public string Param1 { get; set; }
public string Param2 { get; set; }
}
}