初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
+108
@@ -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>
|
||||
+1698
@@ -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
|
||||
}
|
||||
+33
@@ -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>
|
||||
+356
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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>
|
||||
+161
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user