diff --git a/PCL.Core/App/Config.cs b/PCL.Core/App/Config.cs index 1137f4eaa..8f824dc4e 100644 --- a/PCL.Core/App/Config.cs +++ b/PCL.Core/App/Config.cs @@ -565,6 +565,11 @@ partial class HideConfigGroup /// [ConfigItem("LaunchArgumentIndieV2", 4, ConfigSource.Local)] public partial int IndieSolutionV2 { get; set; } + /// + /// 删除隔离实例前的个人文件备份模式。 + /// + [ConfigItem("LaunchArgumentPersonalFilesBackupMode", PersonalFilesBackupMode.AskEveryTime, ConfigSource.Local)] public partial PersonalFilesBackupMode PersonalFilesBackup { get; set; } + /// /// 游戏启动后启动器可见性。 /// diff --git a/PCL.Core/App/ConfigEnums.cs b/PCL.Core/App/ConfigEnums.cs index 6b9eef8b2..09c621c1b 100644 --- a/PCL.Core/App/ConfigEnums.cs +++ b/PCL.Core/App/ConfigEnums.cs @@ -64,6 +64,16 @@ public enum GameProcessPriority RealTime = 4 } +/// +/// 删除实例时的个人文件备份模式。 +/// +public enum PersonalFilesBackupMode +{ + Disabled = 0, + AskEveryTime = 1, + Always = 2 +} + /// /// 游戏启动后启动器可见性 /// diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index c090e7a13..ce1513c7c 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -977,6 +977,14 @@ Saves folder Shortcuts Unfavorite + Back up and delete + Delete without backup + Screenshots were found in instance {0}. Back them up before deletion? Schematics will be backed up at the same time. + Back up screenshots + When screenshots are found, you will be asked whether to back them up before deletion. + Files will be backed up to a folder before deletion. + Failed to back up screenshots or schematics. The instance was kept. See the error details. + Backed up {0} personal file(s) to: {1} @@ -2817,6 +2825,10 @@ Isolate snapshots, pre-releases, old versions, and April Fools' versions from other instances. All instances use the same profile. Saves, mods, resource packs, and other files are shared. If multiple instances with mods installed are present, this may cause mod conflicts. When installing a new instance, the isolation option for the new instance will be automatically set accordingly. To adjust the isolation policy for an existing instance, please go to instance settings. + Protect screenshots when deleting isolated instances + Always back up + Ask every time + Backups are saved to PCL\PersonalFiles in the current Minecraft folder. Schematics are included when screenshots are backed up. Deletion is canceled if the backup fails. @@ -3570,4 +3582,4 @@ Minecraft window loaded: {0} ({1}) Minecraft window maximized: {0} - \ No newline at end of file + diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 9bae29f93..20f2571c7 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -977,6 +977,14 @@ 存档文件夹 快捷方式 从收藏夹中移除 + 备份并删除 + 直接删除 + 检测到实例 {0} 中有截图,是否在删除前备份? 投影原理图也会一并备份。 + 备份截图 + 检测到截图时,删除前会询问是否备份。 + 删除前会备份文件到文件夹。 + 备份截图或投影原理图失败,实例未删除。请查看错误详情。 + 已将 {0} 个回忆文件备份到:{1} @@ -2817,6 +2825,10 @@ 将 Minecraft 快照、预发布版、远古版本、愚人节版本与其他实例进行隔离 所有实例均使用同一档案,存档、模组、资源包等均为公用。 若存在多个安装了模组的实例,可能会导致模组冲突。 当安装新实例时,据此自动设置新实例的隔离选项。 若想调整已有实例的隔离策略,请前往它的实例设置。 + 删除隔离实例时保护截图 + 始终备份 + 每次询问 + 备份文件会保存到当前 Minecraft 文件夹下的 PCL\PersonalFiles。备份截图时也会保存投影原理图;备份失败时会取消删除。 @@ -3570,4 +3582,4 @@ Minecraft 窗口已加载:{0}({1}) 已最大化 Minecraft 窗口:{0} - \ No newline at end of file + diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModPersonalFiles.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModPersonalFiles.cs new file mode 100644 index 000000000..49c30ee2f --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModPersonalFiles.cs @@ -0,0 +1,106 @@ +using System.IO; +using PCL.Core.App; +using PCL.Core.App.Localization; +using PCL.Core.UI; + +namespace PCL; + +public static class ModPersonalFiles +{ + private static string _ArchiveRoot => + Path.GetFullPath(Path.Combine(ModFolder.mcFolderSelected, "PCL", "PersonalFiles")); + + private static (int FileCount, string TargetFolder) _Backup(McInstance instance) + { + var targetFolder = Path.Combine(_ArchiveRoot, instance.Name); + var instancePath = Path.GetFullPath(instance.PathInstance); + if (!instancePath.EndsWith(Path.DirectorySeparatorChar)) instancePath += Path.DirectorySeparatorChar; + if (targetFolder.StartsWith(instancePath, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("回忆文件备份目录不能位于实例目录中"); + + var fileCount = _CopyDirectory(Path.Combine(instance.PathIndie, "screenshots"), + Path.Combine(targetFolder, "screenshots")); + fileCount += _CopyDirectory(Path.Combine(instance.PathIndie, "schematics"), + Path.Combine(targetFolder, "schematics")); + + if (fileCount > 0) + ModBase.Log($"[PersonalFiles] 已备份实例 {instance.Name} 的 {fileCount} 个回忆文件到 {targetFolder}"); + return (fileCount, targetFolder); + } + + public static string? GetDeleteHint() + { + return Config.Launch.PersonalFilesBackup switch + { + PersonalFilesBackupMode.Disabled => null, + PersonalFilesBackupMode.AskEveryTime => Lang.Text("Instance.PersonalFiles.Backup.AskHint"), + _ => Lang.Text("Instance.PersonalFiles.Backup.DeleteHint") + }; + } + + public static bool TryHandleBeforeDelete(McInstance instance) + { + try + { + var backupMode = Config.Launch.PersonalFilesBackup; + if (backupMode == PersonalFilesBackupMode.Disabled) return true; + if (backupMode == PersonalFilesBackupMode.AskEveryTime) + { + if (!_HasScreenshots(instance.PathIndie)) return true; + var promptResult = ModMain.MyMsgBox( + Lang.Text("Instance.PersonalFiles.Backup.Ask.Message", instance.Name), + Lang.Text("Instance.PersonalFiles.Backup.Ask.Title"), + Lang.Text("Instance.PersonalFiles.Backup.Ask.BackupAndDelete"), + Lang.Text("Instance.PersonalFiles.Backup.Ask.DeleteWithoutBackup"), + Lang.Text("Common.Action.Cancel")); + if (promptResult == 2) return true; + if (promptResult != 1) return false; + } + + var result = _Backup(instance); + if (result.FileCount > 0) + HintService.Hint(Lang.Text("Instance.PersonalFiles.Backup.Success", result.FileCount, + result.TargetFolder), HintType.Success); + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, $"备份实例 {instance.Name} 的回忆文件失败", ModBase.LogLevel.Msgbox, + userSummary: Lang.Text("Instance.PersonalFiles.Backup.Failed")); + return false; + } + } + + private static bool _HasScreenshots(string instanceFolder) + { + var screenshotFolder = Path.Combine(instanceFolder, "screenshots"); + return Directory.Exists(screenshotFolder) && + Directory.EnumerateFiles(screenshotFolder, "*", _CreateEnumerationOptions()).Any(); + } + + private static int _CopyDirectory(string sourceFolder, string targetFolder) + { + if (!Directory.Exists(sourceFolder)) return 0; + var fileCount = 0; + foreach (var sourceFile in Directory.EnumerateFiles(sourceFolder, "*", _CreateEnumerationOptions())) + { + var targetFile = Path.Combine(targetFolder, Path.GetRelativePath(sourceFolder, sourceFile)); + ModBase.CopyFile(sourceFile, targetFile); + File.SetCreationTimeUtc(targetFile, File.GetCreationTimeUtc(sourceFile)); + File.SetLastWriteTimeUtc(targetFile, File.GetLastWriteTimeUtc(sourceFile)); + fileCount += 1; + } + + return fileCount; + } + + private static EnumerationOptions _CreateEnumerationOptions() + { + return new EnumerationOptions + { + RecurseSubdirectories = true, + IgnoreInaccessible = false, + AttributesToSkip = FileAttributes.ReparsePoint + }; + } +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs index 673becda2..fd15394d0 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs @@ -751,9 +751,12 @@ private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e) (false, true) => "Instance.Overall.Delete.ConfirmMessagePermanent", (false, false) => "Instance.Overall.Delete.ConfirmMessage" }; + var confirmMessage = Lang.Text(confirmMessageKey, PageInstanceLeft.McInstance.Name); + var backupHint = isIsolatedInstance ? ModPersonalFiles.GetDeleteHint() : null; + if (backupHint is not null) confirmMessage += "\r\n" + backupHint; var confirmResult = ModMain.MyMsgBox( - Lang.Text(confirmMessageKey, PageInstanceLeft.McInstance.Name), + confirmMessage, Lang.Text("Instance.Overall.Delete.ConfirmTitle"), button2: Lang.Text("Common.Action.Cancel"), isWarn: isIsolatedInstance || isShiftPressed @@ -763,6 +766,8 @@ private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e) { case 1: { + if (isIsolatedInstance && + !ModPersonalFiles.TryHandleBeforeDelete(PageInstanceLeft.McInstance)) return; var instancePath = PageInstanceLeft.McInstance.PathInstance; var instanceName = PageInstanceLeft.McInstance.Name; ModBase.IniClearCache(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "options.txt")); diff --git a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs index 8b8ae4149..dbdb886d8 100644 --- a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs @@ -532,17 +532,21 @@ public static void DeleteVersion(MyListItem item, McInstance mcInstance) { var isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); var isHintIndie = mcInstance.state != McInstanceState.Error && - (mcInstance.PathIndie ?? "") != (ModFolder.mcFolderSelected ?? ""); + !string.Equals(mcInstance.PathIndie, ModFolder.mcFolderSelected, + StringComparison.OrdinalIgnoreCase); var confirmMsg = isShiftPressed ? Lang.Text("Select.Instance.Delete.ConfirmPermanentMessage", mcInstance.Name) : Lang.Text("Select.Instance.Delete.ConfirmMessage", mcInstance.Name); var confirmFullMsg = confirmMsg + - (isHintIndie ? "\r\n" + Lang.Text("Select.Instance.Delete.IsolatedWarning") : ""); + (isHintIndie ? "\r\n" + Lang.Text("Select.Instance.Delete.IsolatedWarning") : ""); + var backupHint = isHintIndie ? ModPersonalFiles.GetDeleteHint() : null; + if (backupHint is not null) confirmFullMsg += "\r\n" + backupHint; switch (ModMain.MyMsgBox(confirmFullMsg, Lang.Text("Select.Instance.Delete.ConfirmTitle"), button2: Lang.Text("Common.Action.Cancel"), isWarn: true)) { case 1: { + if (isHintIndie && !ModPersonalFiles.TryHandleBeforeDelete(mcInstance)) return; ModBase.IniClearCache(Path.Combine(mcInstance.PathIndie, "options.txt")); ((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache( mcInstance.PathInstance); diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml index 12ad78264..37c56f94a 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml @@ -161,6 +161,25 @@ + + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs index b8656e5ca..80915483b 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs @@ -54,6 +54,7 @@ public void Reload() TextArgumentTitle.Text = Config.Launch.Title; TextArgumentInfo.Text = Config.Launch.TypeInfo; ComboArgumentIndieV2.SelectedIndex = Config.Launch.IndieSolutionV2; + ComboArgumentPersonalFilesBackup.SelectedIndex = (int)Config.Launch.PersonalFilesBackup; ComboArgumentVisibie.SelectedIndex = (int)Config.Launch.LauncherVisibility; ComboArgumentPriority.SelectedValue = ((int)Config.Launch.ProcessPriority).ToString(); ComboArgumentWindowType.SelectedIndex = (int)Config.Launch.GameWindowMode;