CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
574 lines
20 KiB
C#
574 lines
20 KiB
C#
using System.Collections.Specialized;
|
|
using System.IO;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using System.Windows.Threading;
|
|
using Microsoft.VisualBasic.FileIO;
|
|
using PCL.Core.App.Localization;
|
|
using PCL.Core.UI;
|
|
|
|
namespace PCL;
|
|
|
|
public partial class PageInstanceSaves : IRefreshable
|
|
{
|
|
private readonly DispatcherTimer fileSystemRefreshTimer;
|
|
private readonly DispatcherTimer searchTimer;
|
|
private FileSystemWatcher fileSystemWatcher;
|
|
private bool isLoad;
|
|
|
|
private object quickPlayFeature = false;
|
|
|
|
private List<string> saveFolders = new();
|
|
private string worldPath;
|
|
|
|
public PageInstanceSaves()
|
|
{
|
|
InitializeComponent();
|
|
fileSystemRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
|
|
searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
|
|
Loaded += PageSetupLaunch_Loaded;
|
|
Unloaded += Page_Unloaded;
|
|
fileSystemRefreshTimer.Tick += FileSystemRefreshTimer_Tick;
|
|
searchTimer.Tick += SearchTimer_Tick;
|
|
SearchBox.TextChanged += SearchRun;
|
|
}
|
|
|
|
void IRefreshable.Refresh()
|
|
{
|
|
RefreshSelf();
|
|
}
|
|
|
|
private void RefreshSelf()
|
|
{
|
|
Refresh();
|
|
CheckQuickPlay();
|
|
}
|
|
|
|
public static void Refresh()
|
|
{
|
|
if (ModMain.frmInstanceSaves is not null)
|
|
ModMain.frmInstanceSaves.Reload();
|
|
ModMain.frmInstanceLeft.ItemWorld.Checked = true;
|
|
HintService.Hint(Lang.Text("Instance.Saves.Status.Refreshing"), log: false);
|
|
}
|
|
|
|
private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// 重复加载部分
|
|
PanBack.ScrollToHome();
|
|
worldPath = PageInstanceLeft.McInstance.PathIndie + @"saves\";
|
|
if (!Directory.Exists(worldPath))
|
|
Directory.CreateDirectory(worldPath);
|
|
Reload();
|
|
|
|
// 非重复加载部分
|
|
if (isLoad)
|
|
return;
|
|
isLoad = true;
|
|
CheckQuickPlay();
|
|
|
|
// 初始化文件系统监视器和排序按钮
|
|
SetupFileSystemWatcher();
|
|
BtnSort.Click += BtnSortClick;
|
|
SetSortMethod(_currentSortMethod);
|
|
}
|
|
|
|
private string GetFolderNameFromPath(string fullPath)
|
|
{
|
|
return string.IsNullOrEmpty(fullPath) ? "" :
|
|
fullPath.EndsWith(@"\") ? new DirectoryInfo(fullPath).Parent?.Name : new DirectoryInfo(fullPath).Name;
|
|
}
|
|
|
|
private string GetFileNameFromPath(string fullPath)
|
|
{
|
|
return Path.GetFileName(fullPath);
|
|
}
|
|
|
|
private void SetupFileSystemWatcher()
|
|
{
|
|
if (fileSystemWatcher is not null) fileSystemWatcher.Dispose();
|
|
|
|
// 确保目录存在
|
|
if (!Directory.Exists(worldPath))
|
|
Directory.CreateDirectory(worldPath);
|
|
|
|
fileSystemWatcher = new FileSystemWatcher();
|
|
fileSystemWatcher.Path = worldPath;
|
|
fileSystemWatcher.IncludeSubdirectories = false;
|
|
fileSystemWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.LastWrite;
|
|
|
|
fileSystemWatcher.Created += OnFileSystemChanged;
|
|
fileSystemWatcher.Deleted += OnFileSystemChanged;
|
|
fileSystemWatcher.Renamed += OnFileSystemChanged;
|
|
|
|
fileSystemWatcher.EnableRaisingEvents = true;
|
|
}
|
|
|
|
private void OnFileSystemChanged(object sender, FileSystemEventArgs e)
|
|
{
|
|
fileSystemRefreshTimer.Stop();
|
|
fileSystemRefreshTimer.Start();
|
|
}
|
|
|
|
private void FileSystemRefreshTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
fileSystemRefreshTimer.Stop();
|
|
ModBase.RunInUi(() => Reload(), true);
|
|
}
|
|
|
|
private void Page_Unloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
if (fileSystemWatcher is not null)
|
|
{
|
|
fileSystemWatcher.Created -= OnFileSystemChanged;
|
|
fileSystemWatcher.Deleted -= OnFileSystemChanged;
|
|
fileSystemWatcher.Renamed -= OnFileSystemChanged;
|
|
fileSystemWatcher.Dispose();
|
|
fileSystemWatcher = null;
|
|
}
|
|
|
|
fileSystemRefreshTimer.Stop();
|
|
searchTimer.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 确保当前页面上的信息已正确显示。
|
|
/// </summary>
|
|
public void Reload()
|
|
{
|
|
ModAnimation.AniControlEnabled += 1;
|
|
PanBack.ScrollToHome();
|
|
LoadFileList();
|
|
ModAnimation.AniControlEnabled -= 1;
|
|
}
|
|
|
|
private void RefreshUI()
|
|
{
|
|
try
|
|
{
|
|
if (IsSearching)
|
|
{
|
|
var resultCount = _searchResult is null ? 0 : _searchResult.Count;
|
|
PanListBack.Title = Lang.Text("Instance.Saves.SearchResultTitle", resultCount.ToString());
|
|
}
|
|
else
|
|
{
|
|
PanListBack.Title = Lang.Text("Instance.Saves.SaveListTitle", saveFolders.Count.ToString());
|
|
}
|
|
|
|
if (saveFolders.Count == 0)
|
|
{
|
|
PanNoWorld.Visibility = Visibility.Visible;
|
|
PanContent.Visibility = Visibility.Collapsed;
|
|
PanNoWorld.UpdateLayout();
|
|
}
|
|
else
|
|
{
|
|
PanNoWorld.Visibility = Visibility.Collapsed;
|
|
PanContent.Visibility = Visibility.Visible;
|
|
PanContent.UpdateLayout();
|
|
|
|
var showingSaves = (IsSearching ? _searchResult : saveFolders).ToList();
|
|
|
|
if (showingSaves.Any())
|
|
{
|
|
var sortMethod = GetSortMethod(_currentSortMethod);
|
|
showingSaves.Sort((a, b) => sortMethod(a, b));
|
|
}
|
|
|
|
ModAnimation.AniControlEnabled += 1;
|
|
PanList.Children.Clear();
|
|
|
|
foreach (var curFolder in showingSaves)
|
|
{
|
|
// 检查文件夹是否仍然存在
|
|
if (!Directory.Exists(curFolder)) continue;
|
|
|
|
var saveLogo = Path.Combine(curFolder, "icon.png");
|
|
var tmpCurFolder = curFolder;
|
|
if (File.Exists(saveLogo))
|
|
{
|
|
var target =
|
|
$@"{PageInstanceLeft.McInstance.PathInstance}PCL\ImgCache\{ModBase.GetStringMD5(saveLogo)}.png";
|
|
ModBase.CopyFile(saveLogo, target);
|
|
saveLogo = target;
|
|
}
|
|
else
|
|
{
|
|
saveLogo = ModBase.pathImage + "Icons/NoIcon.png";
|
|
}
|
|
|
|
var worldItem = new MyListItem
|
|
{
|
|
Logo = saveLogo,
|
|
Title = GetFolderNameFromPath(curFolder),
|
|
Info =
|
|
Lang.Text("Instance.Saves.CreationTime", Lang.Date(Directory.GetCreationTime(curFolder), "d"), Lang.Date(Directory.GetLastWriteTime(curFolder), "d")),
|
|
Type = MyListItem.CheckType.Clickable
|
|
};
|
|
worldItem.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
|
|
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
|
|
|
|
var btnOpen = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/folder-open",
|
|
ToolTip = Lang.Text("Common.Action.Open")
|
|
};
|
|
btnOpen.Click += (_, _) => ModBase.OpenExplorer(tmpCurFolder);
|
|
var btnDelete = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/trash-2",
|
|
ToolTip = Lang.Text("Common.Action.Delete")
|
|
};
|
|
btnDelete.Click += (_, _) =>
|
|
{
|
|
worldItem.IsEnabled = false;
|
|
worldItem.Info = Lang.Text("Instance.Saves.Deleting");
|
|
ModBase.RunInNewThread(() =>
|
|
{
|
|
try
|
|
{
|
|
FileSystem.DeleteDirectory(tmpCurFolder, UIOption.OnlyErrorDialogs,
|
|
RecycleOption.SendToRecycleBin);
|
|
HintService.Hint(Lang.Text("Instance.Saves.DeletedToRecycleBin"));
|
|
ModBase.RunInUiWait(() => RemoveItem(worldItem));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.DeleteFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.DeleteFailed"));
|
|
ModBase.RunInUiWait(() => Reload());
|
|
}
|
|
});
|
|
};
|
|
var btnCopy = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/copy",
|
|
ToolTip = Lang.Text("Common.Action.Copy")
|
|
};
|
|
btnCopy.Click += (_, _) =>
|
|
{
|
|
try
|
|
{
|
|
if (Directory.Exists(tmpCurFolder))
|
|
{
|
|
Clipboard.SetFileDropList(new StringCollection { tmpCurFolder });
|
|
HintService.Hint(Lang.Text("Instance.Saves.CopiedToClipboard"));
|
|
HintService.Hint(Lang.Text("Instance.Saves.CopyPasteWarning"));
|
|
}
|
|
else
|
|
{
|
|
HintService.Hint(Lang.Text("Instance.Saves.FolderNotFound"));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.CopyFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.CopyFailed"));
|
|
}
|
|
};
|
|
var btnInfo = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/info",
|
|
ToolTip = Lang.Text("Instance.Saves.Details")
|
|
};
|
|
btnInfo.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
|
|
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
|
|
|
|
var btnLaunch = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/play",
|
|
ToolTip = Lang.Text("Instance.Saves.QuickPlay")
|
|
};
|
|
btnLaunch.Click += (_, _) =>
|
|
{
|
|
var worldName = GetFileNameFromPath(tmpCurFolder);
|
|
var launchOptions = new ModLaunch.McLaunchOptions
|
|
{
|
|
WorldName = worldName,
|
|
instance = PageInstanceLeft.McInstance
|
|
};
|
|
ModLaunch.McLaunchStart(launchOptions);
|
|
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
|
|
};
|
|
if ((bool)quickPlayFeature)
|
|
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo, btnLaunch };
|
|
else
|
|
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo };
|
|
|
|
PanList.Children.Add(worldItem);
|
|
}
|
|
|
|
ModAnimation.AniControlEnabled -= 1;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.RefreshUiFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.RefreshUiFailed"));
|
|
}
|
|
}
|
|
|
|
private void CheckQuickPlay()
|
|
{
|
|
try
|
|
{
|
|
var cur = new ModLaunch.LaunchArgument(PageInstanceLeft.McInstance);
|
|
quickPlayFeature = cur.HasArguments("--quickPlaySingleplayer");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
"检查存档快捷启动失败",
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
|
|
}
|
|
}
|
|
|
|
private void LoadFileList()
|
|
{
|
|
try
|
|
{
|
|
ModBase.Log("[World] 刷新存档文件");
|
|
saveFolders.Clear();
|
|
if (Directory.Exists(worldPath))
|
|
saveFolders = Directory.EnumerateDirectories(worldPath).ToList();
|
|
else
|
|
saveFolders = new List<string>();
|
|
|
|
if (ModBase.modeDebug)
|
|
ModBase.Log("[World] 共发现 " + saveFolders.Count + " 个存档文件夹", ModBase.LogLevel.Debug);
|
|
PanList.Children.Clear();
|
|
CheckQuickPlay();
|
|
|
|
if (ModBase.modeDebug)
|
|
{
|
|
if ((bool)quickPlayFeature)
|
|
ModBase.Log("[World] 该实例支持存档快捷启动", ModBase.LogLevel.Debug);
|
|
else
|
|
ModBase.Log("[World] 该实例不支持存档快捷启动", ModBase.LogLevel.Debug);
|
|
}
|
|
|
|
RefreshUI(); // 确保UI刷新
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.LoadListFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.LoadListFailed"));
|
|
}
|
|
}
|
|
|
|
private void RemoveItem(MyListItem item)
|
|
{
|
|
if (PanList.Children.IndexOf(item) == -1)
|
|
return;
|
|
PanList.Children.Remove(item);
|
|
RefreshUI();
|
|
}
|
|
|
|
private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
ModBase.OpenExplorer(worldPath);
|
|
}
|
|
|
|
private void BtnPaste_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
var files = Clipboard.GetFileDropList();
|
|
var loaders = new List<ModLoader.LoaderBase>();
|
|
loaders.Add(new ModLoader.LoaderTask<int, int>("Copy saves", _ =>
|
|
{
|
|
var copied = 0;
|
|
foreach (var i in files)
|
|
try
|
|
{
|
|
if (Directory.Exists(i))
|
|
{
|
|
if (Directory.Exists(worldPath + GetFolderNameFromPath(i)))
|
|
{
|
|
HintService.Hint(Lang.Text("Instance.Saves.DuplicateFolder", GetFolderNameFromPath(i)));
|
|
}
|
|
else
|
|
{
|
|
ModBase.CopyDirectory(i, worldPath + GetFolderNameFromPath(i));
|
|
copied += 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
HintService.Hint(Lang.Text("Instance.Saves.SourceNotFolder"));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.PasteFolderFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.PasteFolderFailed"));
|
|
}
|
|
|
|
if (copied > 0)
|
|
HintService.Hint(Lang.Text("Instance.Saves.PastedCount", copied.ToString()), HintType.Success);
|
|
ModBase.RunInUi(() => Reload());
|
|
}));
|
|
var loader = new ModLoader.LoaderCombo<int>($"{PageInstanceLeft.McInstance.Name} - {Lang.Text("Instance.Saves.CopySave")}", loaders)
|
|
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
|
|
loader.Start(1);
|
|
ModLoader.LoaderTaskbarAdd(loader);
|
|
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
|
ModMain.frmMain.BtnExtraDownload.Ribble();
|
|
}
|
|
|
|
private void BtnDownloadNew_Click(object sender, MouseButtonEventArgs e)
|
|
{
|
|
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadWorld);
|
|
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
|
|
}
|
|
|
|
#region 搜索和排序
|
|
|
|
private SortMethod _currentSortMethod = SortMethod.FileName;
|
|
private List<string> _searchResult;
|
|
|
|
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text);
|
|
|
|
private enum SortMethod
|
|
{
|
|
FileName,
|
|
CreateTime,
|
|
ModifyTime
|
|
}
|
|
|
|
private string GetSortName(SortMethod method)
|
|
{
|
|
switch (method)
|
|
{
|
|
case SortMethod.FileName:
|
|
{
|
|
return Lang.Text("Instance.Saves.SortFileName");
|
|
}
|
|
case SortMethod.CreateTime:
|
|
{
|
|
return Lang.Text("Instance.Saves.SortCreateTime");
|
|
}
|
|
case SortMethod.ModifyTime:
|
|
{
|
|
return Lang.Text("Instance.Saves.SortModifyTime");
|
|
}
|
|
|
|
default:
|
|
{
|
|
return Lang.Text("Instance.Saves.SortFileName");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetSortMethod(SortMethod target)
|
|
{
|
|
_currentSortMethod = target;
|
|
BtnSort.Text = Lang.Text("Instance.Saves.SortBy", GetSortName(target));
|
|
RefreshUI();
|
|
}
|
|
|
|
private void BtnSortClick(object sender, EventArgs e)
|
|
{
|
|
var body = new ContextMenu();
|
|
foreach (SortMethod i in Enum.GetValues(typeof(SortMethod)))
|
|
{
|
|
var item = new MyMenuItem();
|
|
item.Header = GetSortName(i);
|
|
item.Click += (_, _) => SetSortMethod(i);
|
|
body.Items.Add(item);
|
|
}
|
|
|
|
body.PlacementTarget = (UIElement)sender;
|
|
body.Placement = PlacementMode.Bottom;
|
|
body.IsOpen = true;
|
|
}
|
|
|
|
private void SearchRun(object sender, EventArgs e)
|
|
{
|
|
searchTimer.Stop();
|
|
searchTimer.Start();
|
|
}
|
|
|
|
private void SearchTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
searchTimer.Stop();
|
|
PerformSearch();
|
|
}
|
|
|
|
private void PerformSearch()
|
|
{
|
|
try
|
|
{
|
|
if (IsSearching)
|
|
{
|
|
var queryList = new List<ModBase.SearchEntry<string>>();
|
|
foreach (var saveFolder in saveFolders)
|
|
{
|
|
var folderName = GetFolderNameFromPath(saveFolder);
|
|
var searchSource = new List<ModBase.SearchSource>();
|
|
searchSource.Add(new ModBase.SearchSource(folderName, 1d));
|
|
queryList.Add(new ModBase.SearchEntry<string> { item = saveFolder, searchSource = searchSource });
|
|
}
|
|
|
|
_searchResult = ModBase.Search(queryList, SearchBox.Text, ModBase.MaxLocalSearchDepth, 0.35d).Select(r => r.item).ToList();
|
|
}
|
|
else
|
|
{
|
|
_searchResult = null;
|
|
}
|
|
|
|
RefreshUI();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, Lang.Text("Instance.Saves.SearchError"));
|
|
}
|
|
}
|
|
|
|
private Func<string, string, int> GetSortMethod(SortMethod method)
|
|
{
|
|
switch (method)
|
|
{
|
|
case SortMethod.FileName:
|
|
{
|
|
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
case SortMethod.CreateTime:
|
|
{
|
|
return (a, b) => Directory.GetCreationTime(b).CompareTo(Directory.GetCreationTime(a));
|
|
}
|
|
case SortMethod.ModifyTime:
|
|
{
|
|
return (a, b) => Directory.GetLastWriteTime(b).CompareTo(Directory.GetLastWriteTime(a));
|
|
}
|
|
|
|
default:
|
|
{
|
|
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|