using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using EasyChart; namespace EasyChart.Editor { public partial class EasyChartLibraryWindow { private const string UXML_ROOT_PATH = "Assets/EasyChart/LibraryUxml"; private const string UXML_BACKUPS_SUBFOLDER = "_Backups"; private const string PROFILE_JSON_EXT = ".chartprofile.json"; private void ImportUxmlMirrorToRestoreProfiles() { string projectRoot = Directory.GetCurrentDirectory().Replace('\\', '/'); string defaultAbs = Path.Combine(projectRoot, GetActiveUxmlRootPath().Replace('/', Path.DirectorySeparatorChar)); string picked = EditorUtility.OpenFolderPanel("Import UXML Mirror Root", defaultAbs, string.Empty); if (string.IsNullOrEmpty(picked)) return; string pickedNorm = picked.Replace('\\', '/'); if (!pickedNorm.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("Import", "Please pick a folder inside this Unity project (under the project root).", "OK"); return; } string uxmlRootAssetPath = pickedNorm.Substring(projectRoot.Length + 1); if (!uxmlRootAssetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || !AssetDatabase.IsValidFolder(uxmlRootAssetPath)) { EditorUtility.DisplayDialog("Import", $"Invalid folder: {uxmlRootAssetPath}", "OK"); return; } string profileLibraryRoot = ROOT_PATH; if (uxmlRootAssetPath.StartsWith(UXML_ROOT_PATH, StringComparison.OrdinalIgnoreCase)) { string rel = uxmlRootAssetPath.Substring(UXML_ROOT_PATH.Length).TrimStart('/'); if (!string.IsNullOrEmpty(rel)) { int slash = rel.IndexOf('/'); string libraryName = slash >= 0 ? rel.Substring(0, slash) : rel; if (!string.IsNullOrEmpty(libraryName) && !string.Equals(libraryName, UXML_BACKUPS_SUBFOLDER, StringComparison.OrdinalIgnoreCase)) { profileLibraryRoot = $"{ROOT_PATH}/{libraryName}"; } } } string uxmlRootFullPath = Path.Combine(projectRoot, uxmlRootAssetPath.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(uxmlRootFullPath)) { EditorUtility.DisplayDialog("Import", "Selected folder does not exist on disk.", "OK"); return; } string backupsRoot = (uxmlRootAssetPath + "/" + UXML_BACKUPS_SUBFOLDER).Replace('\\', '/'); string[] files; try { files = Directory.GetFiles(uxmlRootFullPath, "*.uxml", SearchOption.AllDirectories); } catch (Exception e) { EditorUtility.DisplayDialog("Import", e.Message, "OK"); return; } int created = 0; int createdFromJson = 0; int skippedExisting = 0; int skippedInvalid = 0; for (int i = 0; i < files.Length; i++) { string full = files[i]; string fullNorm = full.Replace('\\', '/'); string assetPath; if (fullNorm.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase)) { assetPath = fullNorm.Substring(projectRoot.Length + 1); } else { continue; } if (!assetPath.StartsWith(uxmlRootAssetPath, StringComparison.OrdinalIgnoreCase)) continue; if (assetPath.StartsWith(backupsRoot, StringComparison.OrdinalIgnoreCase)) continue; string rel = assetPath.Substring(uxmlRootAssetPath.Length).TrimStart('/'); if (string.IsNullOrEmpty(rel)) continue; string relDir = Path.GetDirectoryName(rel)?.Replace('\\', '/'); string baseName = Path.GetFileNameWithoutExtension(rel); if (string.IsNullOrEmpty(baseName)) { skippedInvalid++; continue; } string destFolder = string.IsNullOrEmpty(relDir) ? profileLibraryRoot : $"{profileLibraryRoot}/{relDir}"; EnsureFolderExists(destFolder); string destAssetPath = $"{destFolder}/{baseName}.asset"; if (AssetDatabase.LoadAssetAtPath(destAssetPath) != null) { skippedExisting++; continue; } string jsonFullPath = Path.Combine(Path.GetDirectoryName(full) ?? string.Empty, baseName + PROFILE_JSON_EXT); var profile = ScriptableObject.CreateInstance(); bool restoredFromJson = false; if (File.Exists(jsonFullPath)) { try { string json = File.ReadAllText(jsonFullPath); if (!string.IsNullOrEmpty(json)) { EditorJsonUtility.FromJsonOverwrite(json, profile); restoredFromJson = true; } } catch { restoredFromJson = false; } } if (!restoredFromJson) { string text; try { text = File.ReadAllText(full); } catch { skippedInvalid++; continue; } if (!TryParseChartElementStyle(text, out float w, out float h)) { w = 300f; h = 200f; } profile.chartWidth = w; profile.chartHeight = h; } if (string.IsNullOrEmpty(profile.chartName)) profile.chartName = baseName; AssetDatabase.CreateAsset(profile, destAssetPath); created++; if (restoredFromJson) createdFromJson++; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); RefreshTree(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); EditorUtility.DisplayDialog( "Import", $"Restored {created} profile(s) ({createdFromJson} from JSON). Skipped {skippedExisting} existing, {skippedInvalid} invalid.", "OK"); } private void ImportUxmlRestoreSelectedProfile(string profilePath) { if (string.IsNullOrEmpty(profilePath)) return; var profile = AssetDatabase.LoadAssetAtPath(profilePath); if (profile == null) { EditorUtility.DisplayDialog("Import", "No valid ChartProfile selected.", "OK"); return; } string profileName = Path.GetFileNameWithoutExtension(profilePath); string libraryRoot = GetActiveProfileRootPath(); // Calculate relative path string relativePath = profilePath; if (relativePath.StartsWith(libraryRoot)) { relativePath = relativePath.Substring(libraryRoot.Length).TrimStart('/'); } string relativeFolder = Path.GetDirectoryName(relativePath)?.Replace('\\', '/'); // Build UXML path string uxmlRoot = GetActiveUxmlRootPath(); string uxmlFolder = string.IsNullOrEmpty(relativeFolder) ? uxmlRoot : $"{uxmlRoot}/{relativeFolder}"; string uxmlPath = $"{uxmlFolder}/{profileName}.uxml"; string jsonPath = $"{uxmlFolder}/{profileName}{PROFILE_JSON_EXT}"; string projectRoot = Directory.GetCurrentDirectory().Replace('\\', '/'); string jsonFullPath = Path.Combine(projectRoot, jsonPath.Replace('/', Path.DirectorySeparatorChar)); string uxmlFullPath = Path.Combine(projectRoot, uxmlPath.Replace('/', Path.DirectorySeparatorChar)); bool restoredFromJson = false; if (File.Exists(jsonFullPath)) { try { string json = File.ReadAllText(jsonFullPath); if (!string.IsNullOrEmpty(json)) { Undo.RecordObject(profile, "Restore Profile from UXML"); EditorJsonUtility.FromJsonOverwrite(json, profile); EditorUtility.SetDirty(profile); restoredFromJson = true; } } catch (Exception e) { Debug.LogWarning($"[EasyChart] Failed to restore from JSON: {e.Message}"); } } if (!restoredFromJson && File.Exists(uxmlFullPath)) { try { string text = File.ReadAllText(uxmlFullPath); if (TryParseChartElementStyle(text, out float w, out float h)) { Undo.RecordObject(profile, "Restore Profile from UXML"); profile.chartWidth = w; profile.chartHeight = h; EditorUtility.SetDirty(profile); restoredFromJson = true; } } catch (Exception e) { Debug.LogWarning($"[EasyChart] Failed to restore from UXML: {e.Message}"); } } if (restoredFromJson) { AssetDatabase.SaveAssets(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); EditorUtility.DisplayDialog("Import", $"Restored profile '{profileName}' from UXML.", "OK"); } else { EditorUtility.DisplayDialog("Import", $"No UXML/JSON found for profile '{profileName}'.\nExpected: {uxmlPath}", "OK"); } } private void ImportUxmlFromBackup() { string projectRoot = Directory.GetCurrentDirectory().Replace('\\', '/'); string backupRoot = $"{GetActiveUxmlRootPath()}/{UXML_BACKUPS_SUBFOLDER}"; string defaultAbs = Path.Combine(projectRoot, backupRoot.Replace('/', Path.DirectorySeparatorChar)); string picked = EditorUtility.OpenFolderPanel("Select Backup Folder", defaultAbs, string.Empty); if (string.IsNullOrEmpty(picked)) return; string pickedNorm = picked.Replace('\\', '/'); if (!pickedNorm.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("Import", "Please pick a folder inside this Unity project.", "OK"); return; } string uxmlRootAssetPath = pickedNorm.Substring(projectRoot.Length + 1); if (!uxmlRootAssetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || !AssetDatabase.IsValidFolder(uxmlRootAssetPath)) { EditorUtility.DisplayDialog("Import", $"Invalid folder: {uxmlRootAssetPath}", "OK"); return; } string profileLibraryRoot = GetActiveProfileRootPath(); string uxmlRootFullPath = Path.Combine(projectRoot, uxmlRootAssetPath.Replace('/', Path.DirectorySeparatorChar)); string[] files; try { files = Directory.GetFiles(uxmlRootFullPath, "*.uxml", SearchOption.AllDirectories); } catch (Exception e) { EditorUtility.DisplayDialog("Import", e.Message, "OK"); return; } int created = 0; int createdFromJson = 0; int skippedExisting = 0; int skippedInvalid = 0; for (int i = 0; i < files.Length; i++) { string full = files[i]; string fullNorm = full.Replace('\\', '/'); string assetPath; if (fullNorm.StartsWith(projectRoot + "/", StringComparison.OrdinalIgnoreCase)) { assetPath = fullNorm.Substring(projectRoot.Length + 1); } else { continue; } if (!assetPath.StartsWith(uxmlRootAssetPath, StringComparison.OrdinalIgnoreCase)) continue; string rel = assetPath.Substring(uxmlRootAssetPath.Length).TrimStart('/'); if (string.IsNullOrEmpty(rel)) continue; string relDir = Path.GetDirectoryName(rel)?.Replace('\\', '/'); string baseName = Path.GetFileNameWithoutExtension(rel); if (string.IsNullOrEmpty(baseName)) { skippedInvalid++; continue; } string destFolder = string.IsNullOrEmpty(relDir) ? profileLibraryRoot : $"{profileLibraryRoot}/{relDir}"; EnsureFolderExists(destFolder); string destAssetPath = $"{destFolder}/{baseName}.asset"; if (AssetDatabase.LoadAssetAtPath(destAssetPath) != null) { skippedExisting++; continue; } string jsonFullPath = Path.Combine(Path.GetDirectoryName(full) ?? string.Empty, baseName + PROFILE_JSON_EXT); var profile = ScriptableObject.CreateInstance(); bool restoredFromJson = false; if (File.Exists(jsonFullPath)) { try { string json = File.ReadAllText(jsonFullPath); if (!string.IsNullOrEmpty(json)) { EditorJsonUtility.FromJsonOverwrite(json, profile); restoredFromJson = true; } } catch { restoredFromJson = false; } } if (!restoredFromJson) { string text; try { text = File.ReadAllText(full); } catch { skippedInvalid++; continue; } if (!TryParseChartElementStyle(text, out float w, out float h)) { w = 300f; h = 200f; } profile.chartWidth = w; profile.chartHeight = h; } if (string.IsNullOrEmpty(profile.chartName)) profile.chartName = baseName; AssetDatabase.CreateAsset(profile, destAssetPath); created++; if (restoredFromJson) createdFromJson++; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); RefreshTree(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); EditorUtility.DisplayDialog( "Import from Backup", $"Restored {created} profile(s) ({createdFromJson} from JSON). Skipped {skippedExisting} existing, {skippedInvalid} invalid.", "OK"); } private void ImportUnityPackage() { string picked = EditorUtility.OpenFilePanel("Import Unity Package", "", "unitypackage"); if (string.IsNullOrEmpty(picked)) return; AssetDatabase.ImportPackage(picked, true); } private static bool TryParseChartElementStyle(string uxml, out float width, out float height) { width = 0f; height = 0f; if (string.IsNullOrEmpty(uxml)) return false; var m = Regex.Match(uxml, "style=\"([^\"]*)\"", RegexOptions.IgnoreCase); if (!m.Success) return false; string style = m.Groups[1].Value; bool ok = false; var mw = Regex.Match(style, "width\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)px", RegexOptions.IgnoreCase); if (mw.Success && float.TryParse(mw.Groups[1].Value, out var w)) { width = w; ok = true; } var mh = Regex.Match(style, "height\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)px", RegexOptions.IgnoreCase); if (mh.Success && float.TryParse(mh.Groups[1].Value, out var h)) { height = h; ok = true; } return ok; } private static string BuildProfileBackupJson(ChartProfile profile) { if (profile == null) return string.Empty; return EditorJsonUtility.ToJson(profile, true); } private void ExportSelectedProfileUnityPackage() { try { if (EditorApplication.isCompiling || EditorApplication.isUpdating) { EditorUtility.DisplayDialog("Export Package", "Unity is busy (compiling/updating). Please try again after it finishes.", "OK"); return; } if (_selectedProfile == null) { EditorUtility.DisplayDialog("Export Package", "Please select a Chart Profile first.", "OK"); return; } string profilePath = AssetDatabase.GetAssetPath(_selectedProfile); if (string.IsNullOrEmpty(profilePath)) { EditorUtility.DisplayDialog("Export Package", "Selected profile has no asset path.", "OK"); return; } string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string baseName = SanitizeFileName(Path.GetFileNameWithoutExtension(profilePath)); if (string.IsNullOrEmpty(baseName)) baseName = SanitizeFileName(_selectedProfile.name); if (string.IsNullOrEmpty(baseName)) baseName = "ChartProfile"; string destFolder = $"{GetActiveUxmlRootPath()}/{UXML_BACKUPS_SUBFOLDER}/UnityPackages"; EnsureFolderExists(destFolder); string destAssetPath = $"{destFolder}/{baseName}_{timestamp}.unitypackage"; string projectRoot = Directory.GetCurrentDirectory(); string destFullPath = Path.Combine(projectRoot, destAssetPath.Replace('/', Path.DirectorySeparatorChar)); AssetDatabase.ExportPackage( new[] { profilePath }, destFullPath, ExportPackageOptions.IncludeDependencies | ExportPackageOptions.Recurse); EditorUtility.DisplayDialog("Export Package", $"Exported to {destAssetPath}", "OK"); } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] ExportSelectedProfileUnityPackage failed: {ex.Message}\n{ex.StackTrace}"); EditorUtility.DisplayDialog("Export Package", ex.Message, "OK"); } } private void ExportAllProfilesUnityPackage() { try { if (EditorApplication.isCompiling || EditorApplication.isUpdating) { EditorUtility.DisplayDialog("Export Package", "Unity is busy (compiling/updating). Please try again after it finishes.", "OK"); return; } var guids = AssetDatabase.FindAssets("t:ChartProfile", new[] { GetActiveProfileRootPath() }); if (guids == null || guids.Length == 0) { EditorUtility.DisplayDialog("Export Package", "No ChartProfile found under the library root.", "OK"); return; } var assetPaths = new List(guids.Length); for (int i = 0; i < guids.Length; i++) { string p = AssetDatabase.GUIDToAssetPath(guids[i]); if (string.IsNullOrEmpty(p)) continue; assetPaths.Add(p); } if (assetPaths.Count == 0) { EditorUtility.DisplayDialog("Export Package", "No valid ChartProfile asset paths found.", "OK"); return; } string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string destFolder = $"{GetActiveUxmlRootPath()}/{UXML_BACKUPS_SUBFOLDER}/UnityPackages"; EnsureFolderExists(destFolder); string destAssetPath = $"{destFolder}/EasyChartLibraryProfiles_{timestamp}.unitypackage"; string projectRoot = Directory.GetCurrentDirectory(); string destFullPath = Path.Combine(projectRoot, destAssetPath.Replace('/', Path.DirectorySeparatorChar)); AssetDatabase.ExportPackage( assetPaths.ToArray(), destFullPath, ExportPackageOptions.IncludeDependencies | ExportPackageOptions.Recurse); EditorUtility.DisplayDialog("Export Package", $"Exported to {destAssetPath}", "OK"); } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] ExportAllProfilesUnityPackage failed: {ex.Message}\n{ex.StackTrace}"); EditorUtility.DisplayDialog("Export Package", ex.Message, "OK"); } } private void ExportToUxml() { try { if (EditorApplication.isCompiling || EditorApplication.isUpdating) { EditorUtility.DisplayDialog("Export", "Unity is busy (compiling/updating). Please try again after it finishes.", "OK"); return; } if (_selectedProfile == null) { EditorUtility.DisplayDialog("Error", "Please select a Chart Profile first.", "OK"); return; } string profilePath = AssetDatabase.GetAssetPath(_selectedProfile); string exportFolder = Path.GetDirectoryName(profilePath)?.Replace("\\", "/"); if (string.IsNullOrEmpty(exportFolder) || !AssetDatabase.IsValidFolder(exportFolder)) { EditorUtility.DisplayDialog("Error", "Invalid export folder for the selected profile.", "OK"); return; } string baseName = SanitizeFileName(_selectedProfile.name); string fileName = baseName + ".uxml"; string path = $"{exportFolder}/{fileName}"; string content = BuildUxmlContentForProfile(_selectedProfile); string projectRoot = Directory.GetCurrentDirectory(); string fullPath = Path.Combine(projectRoot, path.Replace('/', Path.DirectorySeparatorChar)); WriteTextFileIfChanged(fullPath, content); EditorApplication.delayCall += () => { if (!EditorApplication.isCompiling && !EditorApplication.isUpdating) AssetDatabase.Refresh(); }; EditorUtility.DisplayDialog("Export Successful", $"Exported to {path}", "OK"); } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] ExportToUxml failed: {ex.Message}\n{ex.StackTrace}"); EditorUtility.DisplayDialog("Export", ex.Message, "OK"); } } private static bool WriteTextFileIfChanged(string fullPath, string content) { try { if (File.Exists(fullPath)) { string existing = File.ReadAllText(fullPath); if (string.Equals(existing, content, StringComparison.Ordinal)) { return false; } } File.WriteAllText(fullPath, content); return true; } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] WriteTextFileIfChanged failed: path='{fullPath}' err='{ex.Message}'\n{ex.StackTrace}"); return false; } } private string GetBackupRootFolder(string label) { label = SanitizeFileName(label); string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); return $"{GetActiveUxmlRootPath()}/{UXML_BACKUPS_SUBFOLDER}/{label}_{timestamp}"; } private static string BuildUxmlContentForProfile(ChartProfile profile) { if (profile == null) return string.Empty; string profilePath = AssetDatabase.GetAssetPath(profile); string profileKey = Path.GetFileNameWithoutExtension(profilePath); if (string.IsNullOrEmpty(profileKey)) profileKey = profile.name; string profileGuid = AssetDatabase.AssetPathToGUID(profilePath); return $@" "; } private void ExportAllToUxmlMirror() { ExportFolderToUxmlMirrorInternal(GetActiveProfileRootPath(), GetActiveUxmlRootPath(), pruneOrphans: true); } private void ExportAllToUxmlMirrorBackup() { string backupRoot = GetBackupRootFolder("ExportAll"); ExportFolderToUxmlMirrorInternal(GetActiveProfileRootPath(), backupRoot, pruneOrphans: false); } private void ExportFolderToUxmlMirror(string folderPath) { if (string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath)) return; string activeProfileRoot = GetActiveProfileRootPath(); string activeUxmlRoot = GetActiveUxmlRootPath(); string rel = string.Empty; if (folderPath.StartsWith(activeProfileRoot, StringComparison.OrdinalIgnoreCase)) { rel = folderPath.Substring(activeProfileRoot.Length).TrimStart('/'); } string pruneRoot = string.IsNullOrEmpty(rel) ? activeUxmlRoot : $"{activeUxmlRoot}/{rel}"; ExportFolderToUxmlMirrorInternal(folderPath, activeUxmlRoot, pruneOrphans: true, pruneRoot: pruneRoot); } private void ExportFolderToUxmlMirrorBackup(string folderPath) { if (string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath)) return; string label = Path.GetFileName(folderPath); if (string.IsNullOrEmpty(label)) label = "ExportFolder"; string backupRoot = GetBackupRootFolder(label); ExportFolderToUxmlMirrorInternal(folderPath, backupRoot, pruneOrphans: false); } private void ExportFolderToUxmlMirrorInternal(string folderPath, string destRoot, bool pruneOrphans, string pruneRoot = null) { try { if (EditorApplication.isCompiling || EditorApplication.isUpdating) { EditorUtility.DisplayDialog("Export", "Unity is busy (compiling/updating). Please try again after it finishes.", "OK"); return; } if (string.IsNullOrEmpty(folderPath) || !AssetDatabase.IsValidFolder(folderPath)) return; if (string.IsNullOrEmpty(destRoot)) return; EnsureFolderExists(destRoot); string activeProfileRoot = GetActiveProfileRootPath(); var guids = AssetDatabase.FindAssets("t:ChartProfile", new[] { folderPath }); if (guids == null || guids.Length == 0) { EditorUtility.DisplayDialog("Export", "No ChartProfile found in this folder.", "OK"); return; } var expectedAssetPaths = new HashSet(StringComparer.OrdinalIgnoreCase); var usedThisRun = new HashSet(StringComparer.OrdinalIgnoreCase); int exported = 0; int skippedConflicts = 0; for (int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]); if (string.IsNullOrEmpty(assetPath)) continue; var profile = AssetDatabase.LoadAssetAtPath(assetPath); if (profile == null) continue; string assetFolder = Path.GetDirectoryName(assetPath)?.Replace('\\', '/'); if (string.IsNullOrEmpty(assetFolder)) continue; string relativeFolder = string.Empty; if (assetFolder.StartsWith(activeProfileRoot, StringComparison.OrdinalIgnoreCase)) { relativeFolder = assetFolder.Substring(activeProfileRoot.Length).TrimStart('/'); } string destFolder = string.IsNullOrEmpty(relativeFolder) ? destRoot : $"{destRoot}/{relativeFolder}"; EnsureFolderExists(destFolder); string baseName = SanitizeFileName(Path.GetFileNameWithoutExtension(assetPath)); if (string.IsNullOrEmpty(baseName)) continue; string destAssetPath = $"{destFolder}/{baseName}.uxml"; if (usedThisRun.Contains(destAssetPath)) { skippedConflicts++; continue; } usedThisRun.Add(destAssetPath); expectedAssetPaths.Add(destAssetPath); string projectRoot = Directory.GetCurrentDirectory(); string destFullPath = Path.Combine(projectRoot, destAssetPath.Replace('/', Path.DirectorySeparatorChar)); WriteTextFileIfChanged(destFullPath, BuildUxmlContentForProfile(profile)); string jsonAssetPath = $"{destFolder}/{baseName}{PROFILE_JSON_EXT}"; string jsonFullPath = Path.Combine(projectRoot, jsonAssetPath.Replace('/', Path.DirectorySeparatorChar)); WriteTextFileIfChanged(jsonFullPath, BuildProfileBackupJson(profile)); exported++; } if (pruneOrphans) { PruneOrphanUxmlFiles(string.IsNullOrEmpty(pruneRoot) ? destRoot : pruneRoot, expectedAssetPaths); } EditorApplication.delayCall += () => { if (!EditorApplication.isCompiling && !EditorApplication.isUpdating) AssetDatabase.Refresh(); }; if (skippedConflicts > 0) { EditorUtility.DisplayDialog("Export", $"Exported {exported} UXML file(s). Skipped {skippedConflicts} name conflict(s).", "OK"); } else { EditorUtility.DisplayDialog("Export", $"Exported {exported} UXML file(s).", "OK"); } } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] ExportFolderToUxmlMirrorInternal failed: folder='{folderPath}' destRoot='{destRoot}' pruneOrphans='{pruneOrphans}' err='{ex.Message}'\n{ex.StackTrace}"); EditorUtility.DisplayDialog("Export", ex.Message, "OK"); } } private static void PruneOrphanUxmlFiles(string destRoot, HashSet expectedAssetPaths) { try { if (string.IsNullOrEmpty(destRoot)) return; if (destRoot.IndexOf("/" + UXML_BACKUPS_SUBFOLDER, StringComparison.OrdinalIgnoreCase) >= 0) return; string projectRoot = Directory.GetCurrentDirectory(); string destRootFullPath = Path.Combine(projectRoot, destRoot.Replace('/', Path.DirectorySeparatorChar)); if (!Directory.Exists(destRootFullPath)) return; string[] files; try { files = Directory.GetFiles(destRootFullPath, "*.*", SearchOption.AllDirectories); } catch (Exception e) { Debug.LogError($"[EasyChartLibraryWindow] PruneOrphanUxmlFiles scan failed: root='{destRootFullPath}' err='{e.Message}'\n{e.StackTrace}"); return; } for (int i = 0; i < files.Length; i++) { string full = files[i]; string projectRootNormalized = projectRoot.Replace('\\', '/'); string fullNormalized = full.Replace('\\', '/'); string assetPath; if (fullNormalized.StartsWith(projectRootNormalized + "/", StringComparison.OrdinalIgnoreCase)) { assetPath = fullNormalized.Substring(projectRootNormalized.Length + 1); } else { assetPath = fullNormalized; } if (assetPath.IndexOf("/" + UXML_BACKUPS_SUBFOLDER + "/", StringComparison.OrdinalIgnoreCase) >= 0) continue; bool isUxml = assetPath.EndsWith(".uxml", StringComparison.OrdinalIgnoreCase); bool isJson = assetPath.EndsWith(PROFILE_JSON_EXT, StringComparison.OrdinalIgnoreCase); if (!isUxml && !isJson) continue; if (expectedAssetPaths != null) { if (isUxml && expectedAssetPaths.Contains(assetPath)) continue; if (isJson) { string correspondingUxml = assetPath.Substring(0, assetPath.Length - PROFILE_JSON_EXT.Length) + ".uxml"; if (expectedAssetPaths.Contains(correspondingUxml)) continue; } } AssetDatabase.DeleteAsset(assetPath); } } catch (Exception ex) { Debug.LogError($"[EasyChartLibraryWindow] PruneOrphanUxmlFiles failed: destRoot='{destRoot}' err='{ex.Message}'\n{ex.StackTrace}"); } } } }