using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SettingsManagement;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.Play.Publisher.Editor
{
///
/// Represents an editor window that allows the user to publish a WebGL build of the project to Unity Play
///
public class PublisherWindow : EditorWindow
{
///
/// Name of the tab displayed to a first time user
///
public const string TabIntroduction = "Introduction";
///
/// Name of the tab dsplayed when the user is not logged in
///
public const string TabNotLoggedIn = "NotLoggedIn";
///
/// Name of the tab displayed when WebGL module is not installed
///
public const string TabInstallWebGL = "InstallWebGl";
///
/// Name of the tab displayed when no build is available
///
public const string TabNoBuild = "NoBuild";
///
/// Name of the tab displayed when a build is successfully published
///
public const string TabSuccess = "Success";
///
/// Name of the tab displayed when an error occurs
///
public const string TabError = "Error";
///
/// Name of the tab displayed while uploading a build
///
public const string TabUploading = "Uploading";
///
/// Name of the tab displayed while processing a build
///
public const string TabProcessing = "Processing";
///
/// Name of the tab from which builds can be uploaded
///
public const string TabUpload = "Upload";
///
/// Finds the first open instance of PublisherWindow, if any.
///
///
public static PublisherWindow FindInstance() => Resources.FindObjectsOfTypeAll().FirstOrDefault();
///
/// Holds all the Fronted setup methods of the available tabs
///
static Dictionary tabFrontendSetupMethods;
[UserSetting("Publish WebGL Game", "Show first-time instructions")]
static UserSetting openedForTheFirstTime = new UserSetting(PublisherSettingsManager.instance, "firstTime", true, SettingsScope.Project);
[UserSetting("Publish WebGL Game", "Auto-publish after build is completed")]
static UserSetting autoPublishSuccessfulBuilds = new UserSetting(PublisherSettingsManager.instance, "autoPublish", true, SettingsScope.Project);
///
/// A representation of the AppState
///
internal Store Store
{
get
{
if (m_Store == null)
{
m_Store = CreateStore();
}
return m_Store;
}
}
Store m_Store;
///
/// The active tab in the UI
///
public string CurrentTab { get; private set; }
///
/// Returns true or false depending if localization is still initializing or not
///
public bool IsWaitingForLocalizationToBeReady { get; private set; } = true;
PublisherState currentState;
string previousTab;
string gameTitle = PublisherUtils.DefaultGameName;
bool webGLIsInstalled;
StyleSheet lastCommonStyleSheet; // Dark/Light theme
///
/// Opens the Publisher window
///
///
[MenuItem("Publish/WebGL Project")]
public static PublisherWindow OpenWindow()
{
var window = GetWindow();
window.Show();
return window;
}
void OnEnable()
{
EditorCoroutines.Editor.EditorCoroutineUtility.StartCoroutineOwnerless(DeferredOnEnable());
}
IEnumerator DeferredOnEnable()
{
IsWaitingForLocalizationToBeReady = true;
yield return new EditorCoroutines.Editor.EditorWaitForSeconds(0.5f);
string token = UnityConnectSession.instance.GetAccessToken();
if (token.Length == 0)
{
Store.Dispatch(new NotLoginAction());
}
SetupBackend();
SetupFrontend();
IsWaitingForLocalizationToBeReady = false;
}
void OnDisable()
{
TeardownBackend();
}
void OnBeforeAssemblyReload()
{
SessionState.SetString(typeof(PublisherWindow).Name, EditorJsonUtility.ToJson(Store));
}
static Store CreateStore()
{
var publisherState = JsonUtility.FromJson(SessionState.GetString(typeof(PublisherWindow).Name, "{}"));
return new Store(PublisherReducer.Reducer, publisherState, PublisherMiddleware.Create());
}
void Update()
{
if (IsWaitingForLocalizationToBeReady) { return; }
if (currentState != Store.state.step)
{
string token = UnityConnectSession.instance.GetAccessToken();
if (token.Length != 0)
{
currentState = Store.state.step;
return;
}
Store.Dispatch(new NotLoginAction());
}
RebuildFrontend();
}
void SetupFrontend()
{
titleContent.text = Localization.Tr("WINDOW_TITLE");
minSize = new Vector2(300f, 300f);
maxSize = new Vector2(600f, 600f);
RebuildFrontend();
}
void RebuildFrontend()
{
if (!string.IsNullOrEmpty(Store.state.errorMsg))
{
LoadTab(TabError);
return;
}
if (openedForTheFirstTime)
{
LoadTab(TabIntroduction);
return;
}
if (currentState != Store.state.step)
{
currentState = Store.state.step;
}
bool loggedOut = (currentState == PublisherState.Login);
if (loggedOut)
{
LoadTab(TabNotLoggedIn);
return;
}
if (!webGLIsInstalled)
{
UpdateWebGLInstalledFlag();
LoadTab(TabInstallWebGL);
return;
}
if (!PublisherUtils.ValidBuildExists())
{
LoadTab(TabNoBuild);
return;
}
if (!string.IsNullOrEmpty(Store.state.url))
{
LoadTab(TabSuccess);
return;
}
if (currentState == PublisherState.Upload)
{
LoadTab(TabUploading);
return;
}
if (currentState == PublisherState.Process)
{
LoadTab(TabProcessing);
return;
}
LoadTab(TabUpload);
}
void SetupBackend()
{
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
currentState = Store.state.step;
CurrentTab = string.Empty;
previousTab = string.Empty;
UpdateWebGLInstalledFlag();
tabFrontendSetupMethods = new Dictionary
{
{ TabIntroduction, SetupIntroductionTab },
{ TabNotLoggedIn, SetupNotLoggedInTab },
{ TabInstallWebGL, SetupInstallWebGLTab },
{ TabNoBuild, SetupNoBuildTab },
{ TabSuccess, SetupSuccessTab },
{ TabError, SetupErrorTab },
{ TabUploading, SetupUploadingTab },
{ TabProcessing, SetupProcessingTab },
{ TabUpload, SetupUploadTab }
};
}
void TeardownBackend()
{
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
Store.Dispatch(new DestroyAction());
}
void LoadTab(string tabName)
{
if (!CanSwitchToTab(tabName)) { return; }
previousTab = CurrentTab;
CurrentTab = tabName;
rootVisualElement.Clear();
string uxmlDefinitionFilePath = string.Format("Packages/com.unity.connect.share/UI/{0}.uxml", tabName);
VisualTreeAsset windowContent = AssetDatabase.LoadAssetAtPath(uxmlDefinitionFilePath);
windowContent.CloneTree(rootVisualElement);
//preserve the base style, remove all styles defined in UXML and apply new skin
StyleSheet sheet = rootVisualElement.styleSheets[0];
rootVisualElement.styleSheets.Clear();
rootVisualElement.styleSheets.Add(sheet);
UpdateWindowSkin();
if (tabFrontendSetupMethods == null || tabFrontendSetupMethods[tabName] == null)
{
Debug.LogErrorFormat("Could not find setup method for tab {0}. This can happen when a build process completes. Please close and re-open the WebGL Publisher", tabName);
return;
}
tabFrontendSetupMethods[tabName].Invoke();
}
void UpdateWindowSkin()
{
RemoveStyleSheet(lastCommonStyleSheet, rootVisualElement);
string theme = EditorGUIUtility.isProSkin ? "_Dark" : string.Empty;
string commonStyleSheetFilePath = string.Format("Packages/com.unity.connect.share/UI/Styles{0}.uss", theme);
lastCommonStyleSheet = AssetDatabase.LoadAssetAtPath(commonStyleSheetFilePath);
rootVisualElement.styleSheets.Add(lastCommonStyleSheet);
}
bool CanSwitchToTab(string tabName) { return tabName != CurrentTab; }
#region Tabs Generation
void SetupIntroductionTab()
{
SetupLabel("lblTitle", "INTRODUCTION_TITLE", true);
SetupLabel("lblSubTitle1", "INTRODUCTION_SUBTITLE_1", true);
SetupButton("btnGetStarted", OnGetStartedClicked, true, null, "INTRODUCTION_BUTTON", true);
}
void SetupNotLoggedInTab()
{
SetupLabel("lblTitle", "NOTLOGGEDIN_TITLE", true);
SetupLabel("lblSubTitle1", "NOTLOGGEDIN_SUBTITLE_1", true);
SetupButton("btnSignIn", OnSignInClicked, true, null, "NOTLOGGEDIN_BUTTON", true);
}
void SetupInstallWebGLTab()
{
SetupLabel("lblTitle", "INSTALLWEBGL_TITLE", true);
SetupLabel("lblSubTitle1", "INSTALLWEBGL_SUBTITLE_1", true);
SetupButton("btnOpenInstallGuide", OnOpenInstallationGuideClicked, true, null, "INSTALLWEBGL_BUTTON", true);
}
void SetupNoBuildTab()
{
SetupLabel("lblTitle", "NOBUILD_TITLE", true);
SetupLabel("lblInstructions", "NOBUILD_INSTRUCTIONS", true);
string buildButtonText = autoPublishSuccessfulBuilds ? "NOBUILD_BUTTON_BUILD_AUTOPUBLISH" : "NOBUILD_BUTTON_BUILD_MANUALPUBLISH";
string buildButtonTooltip = autoPublishSuccessfulBuilds ? "NOBUILD_BUTTON_BUILD_AUTOPUBLISH_TOOLTIP" : "NOBUILD_BUTTON_BUILD_MANUALPUBLISH_TOOLTIP";
SetupButton("btnBuild", OnCreateABuildClicked, true, null, Localization.Tr(buildButtonTooltip), buildButtonText, true);
SetupButton("btnLocateExisting", OnLocateBuildClicked, true, null, "NOBUILD_BUTTON_LOCATE", true);
}
void SetupSuccessTab()
{
AnalyticsHelper.UploadCompleted(UploadResult.Succeeded);
FormatGameTitle();
SetupLabel("lblMessage", "SUCCESS_MESSAGE", true);
SetupLabel("lblAdvice", "SUCCESS_ADVICE", true);
SetupLabel("lblLink", "SUCCESS_LINK", rootVisualElement, new PublisherUtils.LeftClickManipulator(OnProjectLinkClicked), true);
SetupButton("btnFinish", OnFinishClicked, true, null, "SUCCESS_BUTTON", true);
OpenConnectUrl(Store.state.url);
}
void SetupErrorTab()
{
SetupLabel("lblTitle", "ERROR_TITLE", true);
SetupLabel("lblError", Store.state.errorMsg);
SetupButton("btnBack", OnBackClicked, true, null, "ERROR_BUTTON", true);
}
void SetupUploadingTab()
{
FormatGameTitle();
SetupButton("btnCancel", OnCancelUploadClicked, true, null, "UPLOADING_BUTTON", true);
}
void SetupProcessingTab()
{
FormatGameTitle();
SetupButton("btnCancel", OnCancelUploadClicked, true, null, "PROCESSING_BUTTON", true);
}
void SetupUploadTab()
{
List existingBuildsPaths = PublisherUtils.GetAllBuildsDirectories();
VisualElement buildsList = rootVisualElement.Query("buildsList");
buildsList.contentContainer.Clear();
VisualTreeAsset containerTemplate = UIElementsUtils.LoadUXML("BuildContainerTemplate");
VisualElement containerInstance;
for (int i = 0; i < PublisherUtils.MaxDisplayedBuilds; i++)
{
containerInstance = containerTemplate.CloneTree().Q("buildContainer");
SetupBuildContainer(containerInstance, existingBuildsPaths[i]);
buildsList.contentContainer.Add(containerInstance);
}
SetupBuildButtonInUploadTab();
ToolbarMenu helpMenu = rootVisualElement.Q("menuHelp");
helpMenu.menu.AppendAction(Localization.Tr("UPLOAD_MENU_BUTTON_SETTINGS"), a => { OnOpenBuildSettingsClicked(); }, a => DropdownMenuAction.Status.Normal);
helpMenu.menu.AppendAction(Localization.Tr("UPLOAD_MENU_BUTTON_LOCATEBUILD"), a => { OnLocateBuildClicked(); }, a => DropdownMenuAction.Status.Normal);
helpMenu.menu.AppendAction(Localization.Tr("UPLOAD_MENU_BUTTON_TUTORIAL"), a => { OnOpenHelpClicked(); }, a => DropdownMenuAction.Status.Normal);
helpMenu.menu.AppendAction(Localization.Tr("UPLOAD_MENU_BUTTON_AUTOPUBLISH"), a => { OnToggleAutoPublish(); }, a => { return GetAutoPublishCheckboxStatus(); }, autoPublishSuccessfulBuilds.value);
//hide the dropdown arrow
IEnumerator helpMenuChildrenEnumerator = helpMenu.Children().GetEnumerator();
helpMenuChildrenEnumerator.MoveNext(); //get to the label (to ignore)
helpMenuChildrenEnumerator.MoveNext(); //get to the dropdown arrow (to hide)
helpMenuChildrenEnumerator.Current.visible = false;
SetupLabel("lblTitle", "UPLOAD_TITLE", true);
}
DropdownMenuAction.Status GetAutoPublishCheckboxStatus()
{
return autoPublishSuccessfulBuilds ? DropdownMenuAction.Status.Checked
: DropdownMenuAction.Status.Normal;
}
static string GetGameTitleFromPath(string buildPath)
{
if (!buildPath.Contains("/")) { return buildPath; }
return buildPath.Split('/').Last();
}
void FormatGameTitle()
{
gameTitle = PublisherUtils.GetFilteredGameTitle(gameTitle);
}
void SetupBuildButtonInUploadTab()
{
string buildButtonText = autoPublishSuccessfulBuilds ? "UPLOAD_BUTTON_BUILD_AUTOPUBLISH" : "UPLOAD_BUTTON_BUILD_MANUALPUBLISH";
SetupButton("btnNewBuild", OnCreateABuildClicked, true, null, buildButtonText, true);
}
#endregion
#region UI Events and Callbacks
void OnBackClicked()
{
Store.Dispatch(new DestroyAction());
LoadTab(previousTab);
}
void OnGetStartedClicked()
{
openedForTheFirstTime.SetValue(false);
}
void OnSignInClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_SignIn", CurrentTab));
UnityConnectSession.instance.ShowLogin();
}
void OnOpenInstallationGuideClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_OpenInstallationGuide", CurrentTab));
Application.OpenURL("https://learn.unity.com/tutorial/fps-mod-share-your-game-on-the-web?projectId=5d9c91a4edbc2a03209169ab#5db306f5edbc2a001f7a307d");
}
void OnOpenHelpClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_OpenHelp", CurrentTab));
Application.OpenURL("https://learn.unity.com/tutorial/fps-mod-share-your-game-on-the-web?projectId=5d9c91a4edbc2a03209169ab#5db306f5edbc2a001f7a307d");
}
void OnToggleAutoPublish()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_ToggleAutoPublish", CurrentTab));
autoPublishSuccessfulBuilds.SetValue(!autoPublishSuccessfulBuilds);
SetupBuildButtonInUploadTab();
}
void OnLocateBuildClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_LocateBuild", CurrentTab));
string lastBuildPath = PublisherUtils.GetFirstValidBuildPath();
if (string.IsNullOrEmpty(lastBuildPath) && PublisherBuildProcessor.CreateDefaultBuildsFolder)
{
lastBuildPath = PublisherBuildProcessor.DefaultBuildsFolderPath;
if (!Directory.Exists(lastBuildPath))
{
Directory.CreateDirectory(lastBuildPath);
}
}
string buildPath = EditorUtility.OpenFolderPanel(Localization.Tr("DIALOG_CHOOSE_FOLDER"), lastBuildPath, string.Empty);
if (string.IsNullOrEmpty(buildPath)) { return; }
if (!PublisherUtils.BuildIsValid(buildPath))
{
Store.Dispatch(new OnErrorAction() { errorMsg = Localization.Tr("ERROR_BUILD_CORRUPTED") });
return;
}
PublisherUtils.AddBuildDirectory(buildPath);
if (CurrentTab != TabUpload) { return; }
SetupUploadTab();
}
void OnOpenBuildSettingsClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_OpenBuildSettings", CurrentTab));
BuildPlayerWindow.ShowBuildPlayerWindow();
}
void OnCreateABuildClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_CreateBuild", CurrentTab));
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.WebGL)
{
if (!ShowSwitchToWebGLPopup()) { return; } //Debug.LogErrorFormat("Switching from {0} to {1}", EditorUserBuildSettings.activeBuildTarget, BuildTarget.WebGL);
}
OnWebGLBuildTargetSet();
}
void OnFinishClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_Finish", CurrentTab));
Store.Dispatch(new DestroyAction());
}
void OnCancelUploadClicked()
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_CancelUpload", CurrentTab));
AnalyticsHelper.UploadCompleted(UploadResult.Cancelled);
Store.Dispatch(new StopUploadAction());
}
void OnOpenBuildFolderClicked(string buildPath)
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_OpenBuildFolder", CurrentTab));
EditorUtility.RevealInFinder(buildPath);
}
void OnPublishClicked(string gameBuildPath, string gameTitle)
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_Publish", CurrentTab));
if (!PublisherUtils.BuildIsValid(gameBuildPath))
{
Store.Dispatch(new OnErrorAction() { errorMsg = Localization.Tr("ERROR_BUILD_CORRUPTED") });
return;
}
this.gameTitle = gameTitle;
FormatGameTitle();
Store.Dispatch(new PublishStartAction() { title = gameTitle, buildPath = gameBuildPath });
}
void OnDeleteClicked(string buildPath, string gameTitle)
{
if (!Directory.Exists(buildPath))
{
Store.Dispatch(new OnErrorAction() { errorMsg = Localization.Tr("ERROR_BUILD_NOT_FOUND") });
return;
}
if (ShowDeleteBuildPopup(gameTitle))
{
AnalyticsHelper.ButtonClicked(string.Format("{0}_Delete_RemoveFromList", CurrentTab));
PublisherUtils.RemoveBuildDirectory(buildPath);
SetupUploadTab();
}
}
internal void OnUploadProgress(int percentage)
{
if (CurrentTab != TabUploading) { return; }
ProgressBar progressBar = rootVisualElement.Query("barProgress");
progressBar.value = percentage;
SetupLabel("lblProgress", string.Format(Localization.Tr("UPLOADING_PROGRESS"), percentage));
}
internal void OnProcessingProgress(int percentage)
{
if (CurrentTab != TabProcessing) { return; }
ProgressBar progressBar = rootVisualElement.Query("barProgress");
progressBar.value = percentage;
SetupLabel("lblProgress", string.Format(Localization.Tr("PROCESSING_PROGRESS"), percentage));
}
internal void OnBuildCompleted(string buildPath)
{
if (autoPublishSuccessfulBuilds)
{
OnPublishClicked(buildPath, GetGameTitleFromPath(buildPath));
}
if (CurrentTab != TabUpload) { return; }
SetupUploadTab();
}
#endregion
#region UI Setup Helpers
void SetupBuildContainer(VisualElement container, string buildPath)
{
if (PublisherUtils.BuildIsValid(buildPath))
{
string gameTitle = GetGameTitleFromPath(buildPath);
SetupButton("btnOpenFolder", () => OnOpenBuildFolderClicked(buildPath), true, container, Localization.Tr("UPLOAD_CONTAINER_BUTTON_OPEN_TOOLTIP"));
SetupButton("btnDelete", () => OnDeleteClicked(buildPath, gameTitle), true, container, Localization.Tr("UPLOAD_CONTAINER_BUTTON_DELETE_TOOLTIP"));
SetupButton("btnShare", () => OnPublishClicked(buildPath, gameTitle), true, container, Localization.Tr("UPLOAD_CONTAINER_BUTTON_PUBLISH_TOOLTIP"), "UPLOAD_CONTAINER_BUTTON_PUBLISH", true);
SetupLabel("lblLastBuildInfo", string.Format(Localization.Tr("UPLOAD_CONTAINER_CREATION_DATE"), File.GetLastWriteTime(buildPath), PublisherUtils.GetUnityVersionOfBuild(buildPath)), container);
SetupLabel("lblGameTitle", gameTitle, container);
SetupLabel("lblBuildSize", string.Format(Localization.Tr("UPLOAD_CONTAINER_BUILD_SIZE"), PublisherUtils.FormatBytes(PublisherUtils.GetFolderSize(buildPath))), container);
container.style.display = DisplayStyle.Flex;
return;
}
SetupButton("btnOpenFolder", null, false, container);
SetupButton("btnDelete", null, false, container);
SetupButton("btnShare", null, false, container);
SetupLabel("lblGameTitle", "-", container);
SetupLabel("lblLastBuildInfo", "-", container);
container.style.display = DisplayStyle.None;
}
void SetupButton(string buttonName, Action onClickAction, bool isEnabled, VisualElement parent, string newText, bool localize)
{
SetupButton(buttonName, onClickAction, isEnabled, parent, string.Empty, newText, localize);
}
void SetupButton(string buttonName, Action onClickAction, bool isEnabled, VisualElement parent = null, string tooltip = "", string newText = "", bool localize = false)
{
parent = parent ?? rootVisualElement;
Button button = parent.Query