Update HotReload
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor {
|
||||
internal static class ReportWindowAPI {
|
||||
public static void OpenBugReport(string title = null, string description = null) {
|
||||
HotReloadBugReportWindow.Open(
|
||||
ReportMode.BugReport,
|
||||
discordUrl: Constants.DiscordInviteUrl,
|
||||
contactUrl: Constants.ContactURL,
|
||||
email: HotReloadPrefs.LicenseEmail,
|
||||
details: description,
|
||||
title: title
|
||||
);
|
||||
}
|
||||
|
||||
public static void OpenFeedback(
|
||||
Func<Report, Task<string>> submitHandler = null,
|
||||
string discordUrl = null,
|
||||
string contactUrl = null,
|
||||
string title = null,
|
||||
string email = null,
|
||||
string feedback = null
|
||||
) {
|
||||
HotReloadBugReportWindow.Open(
|
||||
ReportMode.Feedback,
|
||||
discordUrl: Constants.DiscordInviteUrl,
|
||||
contactUrl: Constants.ContactURL,
|
||||
email: HotReloadPrefs.LicenseEmail
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a7505b2e85b481479de6861e72e954a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,616 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
using SingularityGroup.HotReload.Editor.Localization;
|
||||
using Application = UnityEngine.Application;
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using SingularityGroup.HotReload.Editor.Cli;
|
||||
using SingularityGroup.HotReload.EditorDependencies;
|
||||
using CompressionLevel = System.IO.Compression.CompressionLevel;
|
||||
#endif
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor {
|
||||
|
||||
internal enum ReportMode {
|
||||
BugReport,
|
||||
Feedback
|
||||
}
|
||||
|
||||
internal class HotReloadBugReportWindow : EditorWindow {
|
||||
static List<string> SeverityChoices => new List<string> {
|
||||
Translations.BugReport.SeverityNormal,
|
||||
Translations.BugReport.SeverityLow,
|
||||
Translations.BugReport.SeverityHigh,
|
||||
};
|
||||
|
||||
static List<string> FrequencyChoices => new List<string> {
|
||||
Translations.BugReport.FrequencyFirstTime,
|
||||
Translations.BugReport.FrequencySometimes,
|
||||
Translations.BugReport.FrequencyAlways,
|
||||
};
|
||||
|
||||
[SerializeField] ReportMode _mode = ReportMode.BugReport;
|
||||
|
||||
[SerializeField] VisualTreeAsset visualTree;
|
||||
[SerializeField] StyleSheet styleSheet;
|
||||
|
||||
string _discordUrl;
|
||||
string _contactUrl;
|
||||
|
||||
TextField _titleField;
|
||||
PopupField<string> _severityPopup;
|
||||
PopupField<string> _frequencyPopup;
|
||||
TextField _emailField;
|
||||
IMGUIContainer _detailsContainer;
|
||||
[SerializeField] string _detailsText = "";
|
||||
[SerializeField] string _emailText = "";
|
||||
[SerializeField] string _titleText = "";
|
||||
Label _detailsLabel;
|
||||
Label _validationLabel;
|
||||
Button _submitButton;
|
||||
VisualElement _severitySection;
|
||||
VisualElement _frequencySection;
|
||||
ScrollView _scrollView;
|
||||
VisualElement _detailsSlot;
|
||||
|
||||
VisualElement _successScreen;
|
||||
VisualElement _failureScreen;
|
||||
Label _failureErrorLabel;
|
||||
Button _tabBugReport;
|
||||
Button _tabFeedback;
|
||||
|
||||
bool _isOnResultScreen;
|
||||
|
||||
static Vector2 GetMinSize(ReportMode mode) {
|
||||
return mode == ReportMode.Feedback
|
||||
? new Vector2(420, 500)
|
||||
: new Vector2(420, 620);
|
||||
}
|
||||
|
||||
#region Public API
|
||||
/// <summary>
|
||||
/// Opens the window with the given mode and configuration.
|
||||
/// Prefer using <see cref="ReportWindowAPI"/> for a cleaner call site.
|
||||
/// </summary>
|
||||
public static HotReloadBugReportWindow Open(
|
||||
ReportMode mode,
|
||||
string discordUrl = null,
|
||||
string contactUrl = null,
|
||||
string title = null,
|
||||
string email = null,
|
||||
string details = null
|
||||
) {
|
||||
var wnd = GetWindow<HotReloadBugReportWindow>(utility: true);
|
||||
wnd._mode = mode;
|
||||
wnd._discordUrl = discordUrl;
|
||||
wnd._contactUrl = contactUrl;
|
||||
wnd.titleContent = new GUIContent(
|
||||
mode == ReportMode.Feedback
|
||||
? Translations.BugReport.WindowTitleFeedback
|
||||
: Translations.BugReport.WindowTitleBugReport);
|
||||
wnd.minSize = wnd.maxSize = GetMinSize(mode);
|
||||
|
||||
wnd._titleText = title;
|
||||
wnd._emailText = email;
|
||||
wnd._detailsText = details;
|
||||
|
||||
wnd.RebuildUI();
|
||||
return wnd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefills form fields on an already-open window. Null values are ignored.
|
||||
/// </summary>
|
||||
public void Prefill() {
|
||||
if (_titleText != null && _titleField != null) {
|
||||
_titleField.SetValueWithoutNotify(_titleText);
|
||||
}
|
||||
if (_emailText != null && _emailField != null) {
|
||||
_emailField.SetValueWithoutNotify(_emailText);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
void OnEnable() {
|
||||
RebuildUI();
|
||||
}
|
||||
|
||||
bool HasUnsavedFormContent() {
|
||||
if (_isOnResultScreen) {
|
||||
return false;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(_titleField.value)) {
|
||||
return true;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(_detailsText) && _detailsText != Translations.BugReport.PlaceholderDetails) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnDestroy() {
|
||||
if (_detailsContainer != null && _detailsSlot != null) {
|
||||
_detailsSlot.Remove(_detailsContainer);
|
||||
_detailsContainer = null;
|
||||
}
|
||||
if (!HasUnsavedFormContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool discard = EditorUtility.DisplayDialog(
|
||||
Translations.BugReport.DiscardDialogTitle,
|
||||
Translations.BugReport.DiscardDialogMessage,
|
||||
Translations.BugReport.DiscardDialogConfirm,
|
||||
Translations.BugReport.DiscardDialogCancel);
|
||||
|
||||
if (!discard) {
|
||||
_emailText = _emailField.value;
|
||||
_titleText = _titleField.value;
|
||||
EditorApplication.delayCall += () => {
|
||||
Open(_mode, _discordUrl, _contactUrl);
|
||||
};
|
||||
} else {
|
||||
_detailsText = null;
|
||||
_emailText = null;
|
||||
_titleText = null;
|
||||
}
|
||||
}
|
||||
|
||||
void RebuildUI() {
|
||||
DetachCallbacks();
|
||||
if (visualTree == null) {
|
||||
Log.Warning($"Could not open bug report. Please reach out to our support: {_contactUrl}");
|
||||
return;
|
||||
}
|
||||
|
||||
rootVisualElement.Clear();
|
||||
visualTree.CloneTree(rootVisualElement);
|
||||
if (styleSheet != null) {
|
||||
rootVisualElement.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
QueryElements();
|
||||
AttachIMGUIContainers();
|
||||
ApplyTranslations();
|
||||
CreatePopupFields();
|
||||
ApplyMode();
|
||||
SetupSubmit();
|
||||
SetupResultScreenButtons();
|
||||
FixSingleLineFieldAlignment(_titleField);
|
||||
FixSingleLineFieldAlignment(_emailField);
|
||||
Prefill();
|
||||
AttachCallbacks();
|
||||
|
||||
ShowFormView();
|
||||
}
|
||||
|
||||
void AttachIMGUIContainers() {
|
||||
_detailsSlot = rootVisualElement.Q<VisualElement>("details-field-slot");
|
||||
// Build the IMGUI textarea and inject it
|
||||
_detailsContainer = new IMGUIContainer(DrawDetailsField);
|
||||
_detailsSlot.Add(_detailsContainer);
|
||||
}
|
||||
|
||||
void QueryElements() {
|
||||
_titleField = rootVisualElement.Q<TextField>("title-field");
|
||||
_emailField = rootVisualElement.Q<TextField>("email-field");
|
||||
_detailsLabel = rootVisualElement.Q<Label>("details-label");
|
||||
_validationLabel = rootVisualElement.Q<Label>("validation-label");
|
||||
_submitButton = rootVisualElement.Q<Button>("submit-button");
|
||||
_severitySection = rootVisualElement.Q<VisualElement>("severity-section");
|
||||
_frequencySection = rootVisualElement.Q<VisualElement>("frequency-section");
|
||||
_scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
|
||||
|
||||
_successScreen = rootVisualElement.Q<VisualElement>("success-screen");
|
||||
_failureScreen = rootVisualElement.Q<VisualElement>("failure-screen");
|
||||
_failureErrorLabel = rootVisualElement.Q<Label>("failure-error-label");
|
||||
_tabBugReport = rootVisualElement.Q<Button>("tab-bug-report");
|
||||
_tabFeedback = rootVisualElement.Q<Button>("tab-feedback");
|
||||
}
|
||||
|
||||
private void DrawDetailsField() {
|
||||
// GUILayout.ExpandHeight lets it grow; clamp min so it doesn't collapse
|
||||
_detailsText = EditorGUILayout.TextArea(
|
||||
_detailsText,
|
||||
GUILayout.ExpandWidth(true),
|
||||
GUILayout.MinHeight(160)
|
||||
);
|
||||
}
|
||||
|
||||
void ApplyTranslations() {
|
||||
// Form labels
|
||||
rootVisualElement.Q<Label>(className: "section-label") // Title section — first one
|
||||
?.SetText(Translations.BugReport.LabelTitle);
|
||||
rootVisualElement.Q("severity-section")?.Q<Label>(className: "section-label")
|
||||
?.SetText(Translations.BugReport.LabelIssueSeverity);
|
||||
rootVisualElement.Q("frequency-section")?.Q<Label>(className: "section-label")
|
||||
?.SetText(Translations.BugReport.LabelHowOften);
|
||||
rootVisualElement.Q("tabs")?.Q<Button>("tab-bug-report")
|
||||
?.SetText(Translations.BugReport.TabBugReport);
|
||||
rootVisualElement.Q("tabs")?.Q<Button>("tab-feedback")
|
||||
?.SetText(Translations.BugReport.TabFeedback);
|
||||
|
||||
// Contact section — scoped by class since it has no name
|
||||
var contactSection = rootVisualElement.Q(className: "contact-section");
|
||||
contactSection?.Q<Label>(className: "section-label")?.SetText(Translations.BugReport.LabelContactEmail);
|
||||
contactSection?.Q<Label>(className: "fine-print")?.SetText(Translations.BugReport.LabelContactEmailFinePrint);
|
||||
|
||||
// Submit button
|
||||
_submitButton.text = Translations.BugReport.ButtonSubmit;
|
||||
|
||||
// Success screen
|
||||
var successContent = _successScreen?.Q(className: "result-content");
|
||||
successContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.SuccessTitle);
|
||||
successContent?.Q<Label>(className: "result-message")?.SetText(Translations.BugReport.SuccessMessage);
|
||||
_successScreen?.Q<Button>("success-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
|
||||
|
||||
|
||||
// Failure screen
|
||||
var failureContent = _failureScreen?.Q(className: "result-content");
|
||||
failureContent?.Q<Label>(className: "result-title")?.SetText(Translations.BugReport.FailureTitle);
|
||||
// result-message--error is the dynamic error label, skip it; translate the static one
|
||||
var failureMessages = failureContent?.Query<Label>(className: "result-message").ToList();
|
||||
if (failureMessages != null) {
|
||||
foreach (var lbl in failureMessages) {
|
||||
if (!lbl.ClassListContains("result-message--error")) {
|
||||
lbl.text = Translations.BugReport.FailureMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
_failureScreen?.Q<Button>("failure-discord-button")?.SetText(Translations.BugReport.ButtonJoinDiscord);
|
||||
_failureScreen?.Q<Button>("failure-contact-button")?.SetText(Translations.BugReport.ButtonContactUs);
|
||||
}
|
||||
|
||||
#region View switching
|
||||
void ShowFormView() {
|
||||
_isOnResultScreen = false;
|
||||
_scrollView.style.display = DisplayStyle.Flex;
|
||||
_successScreen.RemoveFromClassList("result-screen--visible");
|
||||
_failureScreen.RemoveFromClassList("result-screen--visible");
|
||||
}
|
||||
|
||||
void ShowSuccessScreen() {
|
||||
_isOnResultScreen = true;
|
||||
_scrollView.style.display = DisplayStyle.None;
|
||||
_tabFeedback.style.display = DisplayStyle.None;
|
||||
_tabBugReport.style.display = DisplayStyle.None;
|
||||
_failureScreen.RemoveFromClassList("result-screen--visible");
|
||||
_successScreen.AddToClassList("result-screen--visible");
|
||||
}
|
||||
|
||||
void ShowFailureScreen(string errorMessage) {
|
||||
_scrollView.style.display = DisplayStyle.None;
|
||||
_tabFeedback.style.display = DisplayStyle.None;
|
||||
_tabBugReport.style.display = DisplayStyle.None;
|
||||
_successScreen.RemoveFromClassList("result-screen--visible");
|
||||
|
||||
if (_failureErrorLabel != null) {
|
||||
if (string.IsNullOrEmpty(errorMessage)) {
|
||||
_failureErrorLabel.style.display = DisplayStyle.None;
|
||||
} else {
|
||||
_failureErrorLabel.text = errorMessage;
|
||||
_failureErrorLabel.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
|
||||
_failureScreen.AddToClassList("result-screen--visible");
|
||||
_isOnResultScreen = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
void ApplyMode() {
|
||||
bool isBugReport = _mode == ReportMode.BugReport;
|
||||
this.minSize = this.maxSize = GetMinSize(_mode);
|
||||
|
||||
if (isBugReport) {
|
||||
_severitySection.style.display = DisplayStyle.Flex;
|
||||
_frequencySection.style.display = DisplayStyle.Flex;
|
||||
_severityPopup.style.display = DisplayStyle.Flex;
|
||||
_frequencyPopup.style.display = DisplayStyle.Flex;
|
||||
_detailsLabel.text = Translations.BugReport.LabelDetails;
|
||||
_tabBugReport.AddToClassList("tab--active");
|
||||
_tabFeedback.RemoveFromClassList("tab--active");
|
||||
if (string.IsNullOrEmpty(_detailsText)) {
|
||||
_detailsText = Translations.BugReport.PlaceholderDetails;
|
||||
_detailsContainer.MarkDirtyRepaint();
|
||||
}
|
||||
} else {
|
||||
_severitySection.style.display = DisplayStyle.None;
|
||||
_frequencySection.style.display = DisplayStyle.None;
|
||||
_severityPopup.style.display = DisplayStyle.None;
|
||||
_frequencyPopup.style.display = DisplayStyle.None;
|
||||
_detailsLabel.text = Translations.BugReport.LabelFeedback;
|
||||
_tabFeedback.AddToClassList("tab--active");
|
||||
_tabBugReport.RemoveFromClassList("tab--active");
|
||||
if (_detailsText == Translations.BugReport.PlaceholderDetails) {
|
||||
_detailsText = string.Empty;
|
||||
_detailsContainer.MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CreatePopupFields() {
|
||||
var severityContainer = rootVisualElement.Q<VisualElement>("severity-container");
|
||||
_severityPopup = new PopupField<string>(
|
||||
string.Empty,
|
||||
SeverityChoices,
|
||||
0);
|
||||
_severityPopup.AddToClassList("input-field");
|
||||
severityContainer.Add(_severityPopup);
|
||||
|
||||
var frequencyContainer = rootVisualElement.Q<VisualElement>("frequency-container");
|
||||
_frequencyPopup = new PopupField<string>(
|
||||
string.Empty,
|
||||
FrequencyChoices,
|
||||
0);
|
||||
_frequencyPopup.AddToClassList("input-field");
|
||||
frequencyContainer.Add(_frequencyPopup);
|
||||
}
|
||||
|
||||
void SelectBugReport() {
|
||||
_mode = ReportMode.BugReport;
|
||||
ApplyMode();
|
||||
}
|
||||
|
||||
void SelectFeedback() {
|
||||
_mode = ReportMode.Feedback;
|
||||
ApplyMode();
|
||||
}
|
||||
|
||||
void AttachCallbacks() {
|
||||
if (_tabBugReport != null) {
|
||||
_tabBugReport.clicked += SelectBugReport;
|
||||
}
|
||||
if (_tabFeedback != null) {
|
||||
_tabFeedback.clicked += SelectFeedback;
|
||||
}
|
||||
if (_emailField != null) {
|
||||
_emailField.RegisterValueChangedCallback(SetEmailText);
|
||||
}
|
||||
if (_titleField != null) {
|
||||
_titleField.RegisterValueChangedCallback(SetTitleText);
|
||||
}
|
||||
}
|
||||
|
||||
void DetachCallbacks() {
|
||||
if (_tabBugReport != null) {
|
||||
_tabBugReport.clicked -= SelectBugReport;
|
||||
}
|
||||
if (_tabFeedback != null) {
|
||||
_tabFeedback.clicked -= SelectFeedback;
|
||||
}
|
||||
if (_emailField != null) {
|
||||
_emailField.UnregisterValueChangedCallback(SetEmailText);
|
||||
}
|
||||
if (_titleField != null) {
|
||||
_titleField.UnregisterValueChangedCallback(SetTitleText);
|
||||
}
|
||||
}
|
||||
|
||||
void SetEmailText(ChangeEvent<string> @event) {
|
||||
_emailText = @event.newValue;
|
||||
}
|
||||
|
||||
void SetTitleText(ChangeEvent<string> @event) {
|
||||
_titleText = @event.newValue;
|
||||
}
|
||||
|
||||
static void FixSingleLineFieldAlignment(TextField field) {
|
||||
field.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||
var input = field.Q(className: "unity-text-field__input");
|
||||
if (input != null) {
|
||||
input.style.alignItems = Align.Center;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#region Validation & submit
|
||||
void SetupSubmit() {
|
||||
_submitButton.clickable.clicked += OnSubmitClicked;
|
||||
}
|
||||
|
||||
async void OnSubmitClicked() {
|
||||
HideValidation();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_titleField.value)) {
|
||||
ShowValidation(Translations.BugReport.ValidationTitleRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
var details = _detailsText;
|
||||
|
||||
var report = new Report {
|
||||
Mode = _mode,
|
||||
Id = Guid.NewGuid().ToString("N").Substring(0, 10),
|
||||
Title = _titleField.value.Trim(),
|
||||
Severity = _severityPopup != null ? _severityPopup.value : null,
|
||||
Frequency = _frequencyPopup != null ? _frequencyPopup.value : null,
|
||||
ContactEmail = _emailField.value != null ? _emailField.value.Trim() : null,
|
||||
Details = details != null ? details.Trim() : null
|
||||
};
|
||||
|
||||
_submitButton.SetEnabled(false);
|
||||
_submitButton.text = Translations.BugReport.ButtonSubmitting;
|
||||
|
||||
string error;
|
||||
try {
|
||||
error = await HandleBugReport(report);
|
||||
} catch (Exception ex) {
|
||||
error = string.Format(Translations.BugReport.SubmitHandlerError, ex.Message);
|
||||
}
|
||||
|
||||
if (this == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_submitButton.SetEnabled(true);
|
||||
_submitButton.text = Translations.BugReport.ButtonSubmit;
|
||||
|
||||
if (error == null) {
|
||||
ShowSuccessScreen();
|
||||
if (report.Mode == ReportMode.BugReport) {
|
||||
// Back up log and patches locally for this bug report
|
||||
// They can be later used to simplify reproducing and fixing the issue, but due to our privacy policy they can't be auto uploaded so we need to ask for them
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
var logName = LogsHelper.FindRecentLog(HotReloadAboutTab.logsPath);
|
||||
try {
|
||||
CreateBugReportZip(
|
||||
Path.Combine(CliUtils.GetCliTempDir(), "Backup"),
|
||||
logName == null ? null : Path.Combine(HotReloadAboutTab.logsPath, logName),
|
||||
Path.Combine(PackageConst.LibraryCachePath, "BugReports", $"{report.Id}.zip")
|
||||
);
|
||||
} catch {
|
||||
// Fail silently. If zip is not available we will have to ask for a reproduce
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
ShowFailureScreen(error);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<string> HandleBugReport(Report report) {
|
||||
var hwId = HotReloadPrefs.HardwareId;
|
||||
if (string.IsNullOrEmpty(hwId)) {
|
||||
hwId = "unknown";
|
||||
}
|
||||
return RequestHelper.SubmitBugReport(new BugReport {
|
||||
reportId = report.Id,
|
||||
label = report.Mode == ReportMode.Feedback ? "Feedback" : "Bug Report",
|
||||
title = report.Title,
|
||||
description = report.Details,
|
||||
email = report.ContactEmail,
|
||||
hwId = hwId,
|
||||
hotReloadVersion = PackageConst.Version,
|
||||
unityVersion = Application.unityVersion,
|
||||
operatingSystemVersionInfo = SystemInfo.operatingSystem,
|
||||
});
|
||||
}
|
||||
|
||||
#if UNITY_2021_3_OR_NEWER
|
||||
private static void CreateBugReportZip(
|
||||
string sourceDir,
|
||||
string sourceFile,
|
||||
string outputZipPath
|
||||
) {
|
||||
var files = Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories);
|
||||
if (files.Length == 0 && sourceFile == null) {
|
||||
return;
|
||||
}
|
||||
Directory.CreateDirectory(new FileInfo(outputZipPath).DirectoryName!);
|
||||
|
||||
using (var stream = new FileStream(outputZipPath, FileMode.Create, FileAccess.Write, FileShare.None, 65536)) {
|
||||
using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: false)) {
|
||||
if (sourceFile != null) {
|
||||
AddFileToArchive(archive, sourceFile, Path.GetFileName(sourceFile));
|
||||
}
|
||||
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) {
|
||||
string relativePath = Path.GetRelativePath(sourceDir, filePath);
|
||||
string entryName = $"Patches/{relativePath}".Replace('\\', '/');
|
||||
AddFileToArchive(archive, filePath, entryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddFileToArchive(ZipArchive archive, string filePath, string entryName) {
|
||||
var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
|
||||
using (var entryStream = entry.Open()) {
|
||||
using (var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite, // allow concurrent writers
|
||||
65536)) {
|
||||
fileStream.CopyTo(entryStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ShowValidation(string message) {
|
||||
_validationLabel.text = message;
|
||||
_validationLabel.AddToClassList("validation-label--visible");
|
||||
}
|
||||
|
||||
void HideValidation() {
|
||||
_validationLabel.RemoveFromClassList("validation-label--visible");
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Result screen buttons
|
||||
void SetupResultScreenButtons() {
|
||||
var successDiscord = rootVisualElement.Q<Button>("success-discord-button");
|
||||
var failureDiscord = rootVisualElement.Q<Button>("failure-discord-button");
|
||||
var failureContact = rootVisualElement.Q<Button>("failure-contact-button");
|
||||
|
||||
if (successDiscord != null) {
|
||||
successDiscord.clickable.clicked += OnDiscordClicked;
|
||||
}
|
||||
|
||||
if (failureDiscord != null) {
|
||||
failureDiscord.clickable.clicked += OnDiscordClicked;
|
||||
}
|
||||
if (failureContact != null) {
|
||||
failureContact.clickable.clicked += OnContactClicked;
|
||||
}
|
||||
|
||||
bool hasDiscord = !string.IsNullOrEmpty(_discordUrl);
|
||||
bool hasContact = !string.IsNullOrEmpty(_contactUrl);
|
||||
|
||||
if (successDiscord != null) {
|
||||
successDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
if (failureDiscord != null) {
|
||||
failureDiscord.style.display = hasDiscord ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
if (failureContact != null) {
|
||||
failureContact.style.display = hasContact ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDiscordClicked() {
|
||||
if (!string.IsNullOrEmpty(_discordUrl)) {
|
||||
Application.OpenURL(_discordUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void OnContactClicked() {
|
||||
if (!string.IsNullOrEmpty(_contactUrl)) {
|
||||
Application.OpenURL(_contactUrl);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal class Report {
|
||||
public string Id;
|
||||
public ReportMode Mode;
|
||||
public string Title;
|
||||
public string Severity;
|
||||
public string Frequency;
|
||||
public string ContactEmail;
|
||||
public string Details;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Small extension to avoid repetitive null checks on label/button text assignment
|
||||
namespace SingularityGroup.HotReload.Editor {
|
||||
internal static class VisualElementExtensions {
|
||||
public static void SetText(this UnityEngine.UIElements.Label label, string text) {
|
||||
if (label != null) label.text = text;
|
||||
}
|
||||
public static void SetText(this UnityEngine.UIElements.Button button, string text) {
|
||||
if (button != null) button.text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cceb96662b8a6841a0c7d4adaf01331
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- m_ViewDataDictionary: {instanceID: 0}
|
||||
- visualTree: {fileID: 9197481963319205126, guid: d9941d55822724b41bff198971988a21, type: 3}
|
||||
- styleSheet: {fileID: 7433441132597879392, guid: b8a805d0a53f99248a999c327b3a151e, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
.root {
|
||||
padding: 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 4px;
|
||||
color: var(--unity-colors-label-text);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
height: 28px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.contact-section {
|
||||
padding: 10px;
|
||||
border-width: 1px;
|
||||
border-color: var(--unity-colors-input_field-border);
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
background-color: rgba(127, 127, 127, 0.08);
|
||||
}
|
||||
|
||||
.fine-print {
|
||||
font-size: 10px;
|
||||
color: var(--unity-colors-label-text-disabled);
|
||||
margin-top: 4px;
|
||||
white-space: normal;
|
||||
-unity-font-style: italic;
|
||||
}
|
||||
|
||||
.details-section {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.details-input {
|
||||
flex-grow: 1;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.details-input > .unity-text-field__input {
|
||||
flex-grow: 1;
|
||||
-unity-text-align: upper-left;
|
||||
padding: 6px 8px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.details-input .unity-text-element {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.validation-label {
|
||||
color: rgb(220, 60, 60);
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
-unity-text-align: middle-center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.validation-label--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 4px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* Placeholder styling */
|
||||
.placeholder-active > .unity-text-field__input > .unity-text-element {
|
||||
color: var(--unity-colors-label-text-disabled);
|
||||
}
|
||||
|
||||
/* ── Result screens ── */
|
||||
|
||||
.result-screen {
|
||||
display: none;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.result-screen--visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
align-items: center;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.result-icon--success {
|
||||
color: rgb(40, 160, 40);
|
||||
}
|
||||
|
||||
.result-icon--failure {
|
||||
color: rgb(200, 40, 40);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 18px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 8px;
|
||||
color: var(--unity-colors-label-text);
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
.result-message {
|
||||
font-size: 12px;
|
||||
color: var(--unity-colors-label-text-disabled);
|
||||
margin-bottom: 6px;
|
||||
-unity-text-align: middle-center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.result-message--error {
|
||||
color: rgb(200, 60, 60);
|
||||
-unity-font-style: italic;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.result-buttons {
|
||||
margin-top: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-button {
|
||||
height: 30px;
|
||||
min-width: 180px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.result-button--primary {
|
||||
background-color: rgb(60, 120, 200);
|
||||
color: rgb(240, 240, 240);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.result-button--primary:hover {
|
||||
background-color: rgb(75, 135, 215);
|
||||
}
|
||||
|
||||
.result-button--secondary {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: var(--unity-colors-label-text-disabled);
|
||||
border-width: 1px;
|
||||
border-color: var(--unity-colors-input_field-border);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.result-button--secondary:hover {
|
||||
color: var(--unity-colors-label-text);
|
||||
border-color: var(--unity-colors-input_field-border-focus);
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
flex-direction: row;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--unity-colors-toolbar-border);
|
||||
background-color: var(--unity-colors-toolbar-background);
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
color: var(--unity-colors-label-text-disabled);
|
||||
padding: 6px 0 5px 0;
|
||||
margin: 0 2px;
|
||||
-unity-font-style: normal;
|
||||
}
|
||||
|
||||
.tab--active {
|
||||
color: var(--unity-colors-label-text);
|
||||
background-color: var(--unity-colors-toolbar-button-background-checked);
|
||||
border-width: 0 0 2px 0;
|
||||
border-color: #3c79f2;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8a805d0a53f99248a999c327b3a151e
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
unsupportedSelectorAction: 0
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
|
||||
|
||||
<ui:VisualElement name="tabs" class="tab-bar">
|
||||
<ui:Button name="tab-bug-report" text="Bug Report" class="tab tab--active" />
|
||||
<ui:Button name="tab-feedback" text="Feedback" class="tab" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Form view -->
|
||||
<ui:ScrollView name="scroll-view" class="root">
|
||||
|
||||
<!-- Title -->
|
||||
<ui:VisualElement class="section">
|
||||
<ui:Label text="Title" class="section-label" />
|
||||
<ui:TextField name="title-field" class="input-field" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Issue Severity (PopupField added from C#, hidden in Feedback mode) -->
|
||||
<ui:VisualElement name="severity-section" class="section">
|
||||
<ui:Label text="Issue Severity" class="section-label" />
|
||||
<ui:VisualElement name="severity-container" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- How often does it happen? (hidden in Feedback mode) -->
|
||||
<ui:VisualElement name="frequency-section" class="section">
|
||||
<ui:Label text="How often does it happen?" class="section-label" />
|
||||
<ui:VisualElement name="frequency-container" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Contact Email -->
|
||||
<ui:VisualElement class="section contact-section">
|
||||
<ui:Label text="Contact Email (optional)" class="section-label" />
|
||||
<ui:TextField name="email-field" class="input-field" />
|
||||
<ui:Label text="This email will only be used to follow up on this bug report and will not be stored or shared for any other purpose." class="fine-print" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Details / Feedback -->
|
||||
<ui:VisualElement class="section details-section">
|
||||
<ui:Label name="details-label" text="Details" class="section-label" />
|
||||
<ui:VisualElement name="details-field-slot" class="details-input"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Validation message -->
|
||||
<ui:Label name="validation-label" class="validation-label" />
|
||||
|
||||
<!-- Submit -->
|
||||
<ui:VisualElement class="submit-row">
|
||||
<ui:Button name="submit-button" text="Submit" class="submit-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
</ui:ScrollView>
|
||||
|
||||
<!-- Success screen (hidden by default) -->
|
||||
<ui:VisualElement name="success-screen" class="result-screen">
|
||||
<ui:VisualElement class="result-content">
|
||||
<ui:Label text="✓" class="result-icon result-icon--success" />
|
||||
<ui:Label text="Report Submitted" class="result-title" />
|
||||
<ui:Label text="Thank you! We've accepted your report and are processing it." class="result-message" />
|
||||
<ui:VisualElement class="result-buttons">
|
||||
<ui:Button name="success-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Failure screen (hidden by default) -->
|
||||
<ui:VisualElement name="failure-screen" class="result-screen">
|
||||
<ui:VisualElement class="result-content">
|
||||
<ui:Label text="✗" class="result-icon result-icon--failure" />
|
||||
<ui:Label text="Submission Failed" class="result-title" />
|
||||
<ui:Label name="failure-error-label" text="" class="result-message result-message--error" />
|
||||
<ui:Label text="Please reach out to us directly so we can help resolve your issue." class="result-message" />
|
||||
<ui:VisualElement class="result-buttons">
|
||||
<ui:Button name="failure-discord-button" text="Join Our Discord" class="result-button result-button--primary" />
|
||||
<ui:Button name="failure-contact-button" text="Contact Us" class="result-button result-button--primary" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
</ui:UXML>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9941d55822724b41bff198971988a21
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
Reference in New Issue
Block a user