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,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
}