/* * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright (C) 2025 Sergej Görzen * This file is part of xAPI4Unity. */ #if UNITY_EDITOR using System; using System.Diagnostics; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using xAPI4Unity.Editor.Settings; using Debug = UnityEngine.Debug; namespace xAPI4Unity.Editor { /// /// Manages the local source view for downloading, patching, or cloning xAPI repositories. /// public class LocalSourceView { /// /// Gets the settings for the local source. /// private LocalSourceSettings _settings => MainSettings.Instance.localSource; // Git helper instance for managing Git operations private readonly GitHelper _git = new GitHelper(DefaultValues.RepoUrl, DefaultValues.AssetRoot); /// /// Initializes a new instance of the class. /// public LocalSourceView() { } /// /// Opens the download URL in the default browser, saving the download to the user's downloads folder. /// private void DownloadToDownloads() { Application.OpenURL(_settings.GetDownloadUrl()); } /// /// Downloads the Unity package of the xAPI repository and processes it into the project. /// private void DownloadUnityPackage() { var branch = _settings.repoBranch ?? DefaultValues.RepoBranch; var destPath = Path.Combine(Application.dataPath, "xapi"); var backupPath = Path.Combine(Application.dataPath, "xapi.backup"); if (Directory.Exists(destPath)) { Directory.Move(destPath, backupPath); } var zipPath = Path.Combine(Application.dataPath, "xapi.zip"); var extractPath = Path.Combine(Application.dataPath, "xAPI4Unity"); _settings.path = destPath; MainSettings.Instance.Save(); var filename = _settings.DownloadFilename; // Download the file FileHelper.Download(_settings.GetDownloadUrl(), zipPath, () => { Debug.Log($"Downloaded '{zipPath}'!"); FileHelper.ExtractZip(zipPath, extractPath); // Extract downloaded zip File.Delete(zipPath); // Remove zip file after extraction var ePath = Path.Combine(extractPath, filename); Directory.Move(ePath, destPath); // Move files to "Assets/xapi" // Export as a .unitypackage and handle cleanup var packageFilename = filename + ".unitypackage"; AssetDatabase.ExportPackage(destPath, packageFilename, ExportPackageOptions.Recurse); var xapiPackagesPath = Path.Combine(Application.dataPath, "xAPI_Packages"); if (!Directory.Exists(xapiPackagesPath)) Directory.CreateDirectory(xapiPackagesPath); var packagePath = Path.Combine(xapiPackagesPath, packageFilename); File.Move(packageFilename, packagePath); File.Delete(destPath); Directory.Delete(extractPath, true); // Clean up extraction if (Directory.Exists(backupPath)) Directory.Move(backupPath, destPath); // Restore backup if applicable AssetDatabase.ImportPackage(packagePath, true); Debug.Log($"Extraction complete to '{extractPath}'!"); AssetDatabase.Refresh(); }); } /// /// Downloads and merges the xAPI repository into the "Assets/xapi" folder. /// private void DownloadToAssetsAndPatch() { var filename = _settings.DownloadFilename; var destPath = Path.Combine(Application.dataPath, "xapi"); var zipPath = Path.Combine(Application.dataPath, "xapi.zip"); var extractPath = Path.Combine(Application.dataPath, filename); var mergeReportPath = Path.Combine(destPath, DateTime.Now.ToString("yyyyMMddhhmmss") + "-xapi-report.json"); _settings.path = destPath; MainSettings.Instance.Save(); if (!Directory.Exists(destPath)) { DownloadToAssets(); return; } var url = _settings.GetDownloadUrl(); FileHelper.Download(url, zipPath, () => { try { Debug.Log($"Downloaded '{url}' to '{zipPath}'."); FileHelper.ExtractZip(zipPath, Application.dataPath); File.Delete(zipPath); // Remove the downloaded zip if (!Directory.Exists(destPath)) Directory.CreateDirectory(destPath); var mergeReport = MergeXapiFolder(extractPath, destPath); File.WriteAllText(mergeReportPath, JsonUtility.ToJson(mergeReport, true)); Directory.Delete(extractPath, true); // Clean up extracted files Debug.Log($"Merge Copied: {mergeReport.copied}, Conflicts: {mergeReport.conflicts}, Skipped: {mergeReport.skipped}. Merge report at {mergeReportPath}."); EditorApplication.delayCall += AssetDatabase.Refresh; } catch (Exception ex) { Debug.LogError($"Download or merge failed: {ex.Message}"); } }); } /// /// Merges the contents of the extracted xAPI folder into the destination directory. /// private static MergeReport MergeXapiFolder(string sourceDir, string destDir) { var mergeReport = new MergeReport(Path.GetDirectoryName(destDir)); foreach (var sourceFilePath in Directory.GetFiles(sourceDir, "*.json", SearchOption.AllDirectories)) { var relativePath = FileHelper.GetRelativePath(sourceDir, sourceFilePath).Replace("\\", "/"); var destFilePath = Path.Combine(destDir, relativePath); if (!File.Exists(destFilePath)) { FileHelper.EnsurePath(destFilePath); // Ensure all directories exist before moving the file File.Move(sourceFilePath, destFilePath); // Move new file into place mergeReport.AddCopy(relativePath); } else { // Check if files have conflict var sourceBytes = File.ReadAllBytes(sourceFilePath); var destBytes = File.ReadAllBytes(destFilePath); if (!sourceBytes.SequenceEqual(destBytes)) { mergeReport.AddConflict(relativePath); File.Move(sourceFilePath, destFilePath.Replace(".json", ".json-conflict")); // Rename conflicting files } else { mergeReport.AddSkip(relativePath); // Existing files are skipped if identical } } } return mergeReport; } /// /// Downloads the xAPI repository directly into the "Assets/xapi" folder for a fresh import. /// private void DownloadToAssets() { var destPath = Path.Combine(Application.dataPath, "xapi"); if (Directory.Exists(destPath)) { Debug.Log($"The folder '{destPath}' already exists. Delete it and retry."); return; } var zipPath = Path.Combine(Application.dataPath, "xapi.zip"); var extractPath = Path.Combine(Application.dataPath, "__ExtractedXAPI__"); _settings.path = destPath; MainSettings.Instance.Save(); FileHelper.Download(_settings.GetDownloadUrl(), zipPath, () => { FileHelper.ExtractZip(zipPath, extractPath); File.Delete(zipPath); // Cleanup zip file Directory.Move(Path.Combine(extractPath, _settings.DownloadFilename), destPath); Directory.Delete(extractPath, true); // Cleanup extracted folder Debug.Log($"Extraction complete to '{destPath}'!"); AssetDatabase.Refresh(); }); } /// /// Displays the interface for selecting and managing the local path for the xAPI repository. /// private void LocalPathView() { CustomGUI.Horizontal.Wrap(() => { if (string.IsNullOrEmpty(_settings.path)) _settings.path = DefaultValues.RepoPath; _settings.path = CustomGUI.TextField("Path of local copy", _settings.path, Path.GetDirectoryName(_settings.path)); if (CustomGUI.Button("Browse path")) { _settings.path = EditorUtility.OpenFolderPanel("Browse xAPI Definitions root path", _settings.path, "xapi"); if (string.IsNullOrEmpty(_settings.path)) _settings.path = _git.RepoDirectory; MainSettings.Instance.Save(); GUI.FocusControl(null); } CustomGUI.Disabled(!_git.IsCloned, () => { if (CustomGUI.Button("Open folder")) { Process.Start(_settings.path); } }); }); } /// /// Updates the xAPI folder based on the selected option in the settings. /// public void UpdateXApiFolder() { if (_settings.selectedOptionIndex == 0) { DownloadToAssetsAndPatch(); } else if (_settings.selectedOptionIndex == 1) { DownloadToDownloads(); } else if (_settings.selectedOptionIndex == 2) { Debug.LogError("Invalid Option 3. Please use Option 1 or 2."); } } /// /// Renders the LocalSourceView interface and performs the respective actions based on user interactions. /// public void Show(Action callback) { LocalPathView(); var hasDefinitionsFolder = GitHelper.HasDefinitionsFolder(_settings.path); if (!hasDefinitionsFolder) { CustomGUI.Horizontal.Wrap(() => { CustomGUI.Error( $"The path '{_settings.path}' has no 'definitions' folder. Make sure you extracted the repository content correctly."); }); } // Display options for managing the xAPI source _settings.selectedOptionIndex = CustomGUI.Tabs(_settings.selectedOptionIndex, new[] { new CustomGUI.Tab("(Option 1) Patch a local copy into 'Assets' folder", () => { CustomGUI.Subtitle("Local copy in Assets instructions"); _settings.repoDownloadUrl = CustomGUI.TextField("xAPI Repository URL", _settings.repoDownloadUrl, DefaultValues.RepoDownloadUrl); CustomGUI.Description("Change this only if working with a fork of the repository."); _settings.repoBranch = CustomGUI.TextField("Branch", _settings.repoBranch, DefaultValues.RepoBranch); CustomGUI.Bullets( "Backup your 'xapi' folder if not using git.", $"Download and merge the file from '{_settings.GetDownloadUrl()}' into 'Assets/xapi'.", "Enjoy!" ); // Trigger download or patching if (CustomGUI.Button("Download and Patch into Assets folder")) { DownloadToAssetsAndPatch(); callback(); } }), new CustomGUI.Tab("(Option 2) Download a local copy", () => { CustomGUI.Subtitle("Local copy instructions"); _settings.repoDownloadUrl = CustomGUI.TextField("xAPI Repository URL", _settings.repoDownloadUrl, DefaultValues.RepoDownloadUrl); CustomGUI.Description("Change this only if working with a fork of the repository."); _settings.repoBranch = CustomGUI.TextField("Branch", _settings.repoBranch, DefaultValues.RepoBranch); CustomGUI.Bullets( $"Download from '{_settings.GetDownloadUrl()}'.", "Extract the content locally.", $"Ensure the folder '{_settings.path}' has a 'definitions' directory." ); if (CustomGUI.Button("Download xapi.zip")) { DownloadToDownloads(); } }), new CustomGUI.Tab("(Option 3) Clone into selected path", () => { CustomGUI.Subtitle("Git Settings"); CustomGUI.Disabled(_git.IsBusy, () => { _settings.repoUrl = CustomGUI.TextField("Git repository", _settings.repoUrl, DefaultValues.RepoUrl); _settings.repoBranch = CustomGUI.TextField("Git branch", _settings.repoBranch, default); CustomGUI.Description( "If 'Git branch' is empty, a new branch will be created automatically. You can rename it later."); }); CustomGUI.Horizontal.Wrap(() => { _git.Url = _settings.repoUrl; _git.Directory = Path.GetDirectoryName(_settings.path); CustomGUI.Disabled(_git.IsCloned, () => { var parent = Directory.GetParent(Application.dataPath); if (parent != null) { var projectRoot = parent.ToString(); var projectHasGit = Directory.Exists(Path.Combine(projectRoot, ".git")); if (projectHasGit) { if (CustomGUI.StateButton("Clone as submodule", _git.IsBusy, "Cloning...")) { _git.SubmoduleClone(_settings.GetNewBranchName()); _settings.path = _git.RepoDirectory; callback(); } } else { if (CustomGUI.StateButton("Clone repository", _git.IsBusy, "Cloning...")) { _git.Clone(_settings.GetNewBranchName()); _settings.path = _git.RepoDirectory; callback(); } } } if (!_git.IsCloned) { const string errorMessage = "You need a local copy of the xAPI registry. Browse in `xAPI4Unity > Settings` for it or clone using the 'Clone repository' option."; CustomGUI.Error(errorMessage); Debug.LogError(errorMessage); } }); }); }) }, newIndex => { _settings.selectedOptionIndex = newIndex; MainSettings.Instance.Save(); }); } } } #endif