[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/unitycoder/UnityLauncherPro/dev/UnityLauncherPro/Tools.cs [Back]  [Original]

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using UnityLauncherPro.Helpers;

namespace UnityLauncherPro
{
    public static class Tools
    {
        const int SW_RESTORE = 9;

        [DllImport("user32", CharSet = CharSet.Unicode)]
        static extern IntPtr FindWindow(string cls, string win);
        [DllImport("user32")]
        static extern IntPtr SetForegroundWindow(IntPtr hWnd);
        [DllImport("user32")]
        static extern bool IsIconic(IntPtr hWnd);
        [DllImport("user32")]
        static extern bool OpenIcon(IntPtr hWnd);
        [DllImport("user32")]
        private static extern bool ShowWindow(IntPtr handle, int nCmdShow);

        // reference to already running webgl server processes and ports
        static Dictionary webglServerProcesses = new Dictionary();

        // returns last modified date for file (or null if cannot get it)
        public static DateTime? GetLastModifiedTime(string path)
        {
            if (File.Exists(path) == true || Directory.Exists(path) == true)
            {
                DateTime modification = File.GetLastWriteTime(path);
                return modification;
            }
            else
            {
                return null;
            }
        }

        /// 
        /// parse project version from ProjectSettings/ data
        /// 
        /// project base path
        /// 
        public static string GetProjectVersion(string path)
        {
            string version = null;

            if (File.Exists(Path.Combine(path, "ProjectVersionOverride.txt")))
            {
                version = File.ReadAllText(Path.Combine(path, "ProjectVersionOverride.txt"));
            }
            else if (Directory.Exists(Path.Combine(path, "ProjectSettings")))
            {
                var versionPath = Path.Combine(path, "ProjectSettings", "ProjectVersion.txt");
                if (File.Exists(versionPath) == true) // 5.x and later
                {
                    var data = File.ReadAllLines(versionPath);

                    if (data != null && data.Length > 0)
                    {
                        var dd = data[0];
                        // check string
                        if (dd.Contains("m_EditorVersion"))
                        {
                            var t = dd.Split(new string[] { "m_EditorVersion: " }, StringSplitOptions.None);
                            if (t != null && t.Length > 0)
                            {
                                version = t[1].Trim();
                            }
                            else
                            {
                                throw new InvalidDataException("invalid version data:" + data);
                            }
                        }
                        else
                        {
                            Console.WriteLine("Cannot find m_EditorVersion in '" + versionPath + "'.\n\nFile Content:\n" + string.Join("\n", data).ToString());
                        }
                    }
                    else
                    {
                        Console.WriteLine("Invalid projectversion data found in '" + versionPath + "'.\n\nFile Content:\n" + string.Join("\n", data).ToString());
                    }
                }
                else // maybe its 4.x?
                {
                    versionPath = Path.Combine(path, "ProjectSettings", "ProjectSettings.asset");
                    if (File.Exists(versionPath) == true)
                    {
                        // first try if its ascii format
                        var data = File.ReadAllLines(versionPath);
                        if (data != null && data.Length > 0 && data[0].IndexOf("YAML") > -1) // we have ascii
                        {
                            // check library if available
                            var newVersionPath = Path.Combine(path, "Library", "AnnotationManager");
                            if (File.Exists(newVersionPath) == true)
                            {
                                versionPath = newVersionPath;
                            }
                        }

                        // try to get version data out from binary asset
                        var binData = File.ReadAllBytes(versionPath);
                        if (binData != null && binData.Length > 0)
                        {
                            int dataLen = 7;
                            int startIndex = 20;
                            var bytes = new byte[dataLen];
                            for (int i = 0; i < dataLen; i++)
                            {
                                bytes[i] = binData[startIndex + i];
                            }
                            var vertemp = Encoding.UTF8.GetString(bytes);
                            // probably failed if no dots
                            if (vertemp.IndexOf(".") > -1) version = vertemp;

                        }
                        // if still nothing, TODO probably could find closer version info, if know what features were added to playersettings.assets and checking serializedVersion: .. number
                    }
                }
            }

            return version;
        }

        internal static string ReadProjectName(string projectPath)
        {
            string results = null;
            var versionPath = Path.Combine(projectPath, "ProjectSettings", "ProjectSettings.asset");
            if (File.Exists(versionPath) == true) // 5.x and later
            {
                var data = File.ReadAllLines(versionPath);

                if (data != null && data.Length > 0)
                {
                    for (int i = 0; i < data.Length; i++)
                    {
                        // check row
                        if (data[i].IndexOf("productName: ") > -1)
                        {
                            var t = data[i].Split(new string[] { "productName: " }, StringSplitOptions.None);
                            if (t != null && t.Length > 0)
                            {
                                results = t[1].Trim();
                                break;
                            }
                            else
                            {
                                throw new InvalidDataException("invalid productName data:" + data);
                            }
                        }
                    }
                }
            }
            return results;
        }

        // returns unity version number string from file
        public static string GetFileVersionData(string path)
        {
            FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(path);
            var ver = fvi.ProductName;
            if (string.IsNullOrEmpty(ver) == true)
            {
                ver = fvi.FileDescription;
                if (string.IsNullOrEmpty(ver) == true) return null;
                ver = ver.Replace("Installer", "").Trim();
            }
            var res = ver.Replace("(64-bit)", "").Replace("(32-bit)", "").Replace("Unity", "").Trim();
            return res;
        }

        public static void ExploreFolder(string path)
        {
            if (path != null)
            {
                if (LaunchExplorer(path) == false)
                {
                    //SetStatus("Error> Directory not found: " + folder);
                }
            }
        }

        // this runs before unity editor starts, so the project is not yet in registry (unless it already was there)
        public static void AddProjectToHistory(string projectPath)
        {
            // fix backslashes
            projectPath = projectPath.Replace('\\', '/');

            if (Properties.Settings.Default.projectPaths.Contains(projectPath) == false)
            {
                // TODO do we need to add as first?
                Properties.Settings.Default.projectPaths.Insert(0, projectPath);

                // remove last item, if too many
                if (Properties.Settings.Default.projectPaths.Count > MainWindow.maxProjectCount)
                {
                    Properties.Settings.Default.projectPaths.RemoveAt(Properties.Settings.Default.projectPaths.Count - 1);
                }

                //Console.WriteLine("AddProjectToHistory, count: " + Properties.Settings.Default.projectPaths.Count);

                // TODO no need to save everytime?
                Properties.Settings.Default.Save();

                // TODO need to add into recent grid also? if old items disappear?
            }
        }

        // NOTE holding alt key (when using alt+o) brings up unity project selector
        public static Process LaunchProject(Project proj, DataGrid dataGridRef = null, bool useInitScript = false, bool upgrade = false)
        {
            if (proj == null) return null;

            Console.WriteLine("Launching project " + proj?.Title + " at " + proj?.Path);

            if (Directory.Exists(proj.Path) == false) return null;

            // add this project to recent projects in preferences TODO only if enabled +40 projecs
            AddProjectToHistory(proj.Path);

            // check if this project path has unity already running? (from process) 
            // NOTE this check only works if previous unity instance was started while we were running
            if (ProcessHandler.IsRunning(proj.Path))
            {
                Console.WriteLine("Project is already running, lets not launch unity.. because it opens Hub");
                BringProcessToFront(ProcessHandler.Get(proj.Path));
                return null;
            }
            else
            {
                // TODO check lock file?
            }

            // there is no assets path, probably we want to create new project then
            var assetsFolder = Path.Combine(proj.Path, "Assets");
            if (Directory.Exists(assetsFolder) == false)
            {
                // TODO could ask if want to create project..?
                Directory.CreateDirectory(assetsFolder);
            }

            // when opening project, check for crashed backup scene first
            var cancelLaunch = CheckCrashBackupScene(proj.Path);
            if (cancelLaunch == true)
            {
                return null;
            }

            // if its upgrade, we dont want to check current version
            if (upgrade == false)
            {
                // check if project version has changed? (list is not updated, for example pulled new version from git)
                var version = GetProjectVersion(proj.Path);
                if (string.IsNullOrEmpty(version) == false && version != proj.Version)
                {
                    Console.WriteLine("Project version has changed from " + proj.Version + " to " + version);
                    proj.Version = version;
                }
            }

            // check if we have this unity version installed
            var unityExePath = GetUnityExePath(proj.Version);
            if (unityExePath == null)
            {
                DisplayUpgradeDialog(proj, null, useInitScript);
                return null;
            }

            Process newProcess = new Process();
            try
            {
                var cmd = "\"" + unityExePath + "\"";
                newProcess.StartInfo.FileName = cmd;

                var unitycommandlineparameters = " -projectPath " + "\"" + proj.Path + "\"";

                string customArguments = proj.Arguments;
                if (string.IsNullOrEmpty(customArguments) == false)
                {
                    unitycommandlineparameters += " " + customArguments;
                }

                string projTargetPlatform = proj.TargetPlatform;
                if (string.IsNullOrEmpty(projTargetPlatform) == false)
                {
                    unitycommandlineparameters += " -buildTarget " + projTargetPlatform;
                }

                if (useInitScript == true)
                {
                    unitycommandlineparameters += " -executeMethod UnityLauncherProTools.InitializeProject.Init";
                }

                Console.WriteLine("Start process: " + cmd + " " + unitycommandlineparameters);

                // TODO load custom settings per project
                //string userSettingsFolder = Path.Combine(proj.Path, "UserSettings");
                //string userSettingsPath = Path.Combine(userSettingsFolder, "ULPSettings.txt");
                //if (File.Exists(userSettingsPath))
                //{
                //    var rawSettings = File.ReadAllLines(userSettingsPath);
                //    // needed for env vars.
                //    newProcess.StartInfo.UseShellExecute = false;
                //    foreach (var row in rawSettings)
                //    {
                //        var split = row.Split('=');
                //        if (split.Length == 2)
                //        {
                //            var key = split[0].Trim();
                //            var value = split[1].Trim();
                //            if (string.IsNullOrEmpty(key) == false && string.IsNullOrEmpty(value) == false)
                //            {
                //                //Console.WriteLine("key: " + key + " value: " + value);
                //                //newProcess.StartInfo.EnvironmentVariables[key] = value;
                //                //System.Environment.SetEnvironmentVariable(key, value, EnvironmentVariableTarget.Machine);
                //                var dict = newProcess.StartInfo.EnvironmentVariables;
                //                // print all
                //                foreach (System.Collections.DictionaryEntry de in dict)
                //                {
                //                    Console.WriteLine("  {0} = {1}", de.Key, de.Value);
                //                }
                //                // check if key exists
                //                if (dict.ContainsKey(key) == true)
                //                {
                //                    // modify existing
                //                    //dict[key] = value;
                //                    newProcess.StartInfo.EnvironmentVariables.Remove(key);
                //                    newProcess.StartInfo.EnvironmentVariables.Add(key, value);
                //                }
                //                else
                //                {
                //                    // add new
                //                    dict.Add(key, value);
                //                }
                //                //newProcess.StartInfo.EnvironmentVariables.
                //                //if (newProcess.StartInfo.EnvironmentVariables.ContainsKey(key))
                //                //{
                //                //    Console.WriteLine("exists: "+key);
                //                //    // Test Modify the existing environment variable
                //                //    newProcess.StartInfo.EnvironmentVariables[key] = "...";
                //                // this works, maybe because its not a system variable?
                //                //newProcess.StartInfo.EnvironmentVariables["TESTTEST"] = "...";
                //                //}
                //                //else
                //                //{
                //                //    Console.WriteLine("add new: "+ value);
                //                //    // Optionally, add the environment variable if it does not exist
                //                //    newProcess.StartInfo.EnvironmentVariables.Add(key, value);
                //                //}
                //                Console.WriteLine("custom row: " + row + " key=" + key + " value:" + value);
                //            }
                //        }
                //    }
                //}

                newProcess.StartInfo.Arguments = unitycommandlineparameters;
                newProcess.EnableRaisingEvents = true;
                //newProcess.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; // needed for unity 2023 for some reason? (otherwise console popups briefly), Cannot use this, whole Editor is invisible then
                newProcess.Start();

                if (Properties.Settings.Default.closeAfterProject)
                {
                    Environment.Exit(0);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }

            // NOTE move project as first, since its opened, disabled for now, since its too jumpy..
            //MainWindow wnd = (MainWindow)Application.Current.MainWindow;
            //wnd.MoveRecentGridItem(0);

            ProcessHandler.Add(proj, newProcess);
            return newProcess;
        }

        static bool CheckCrashBackupScene(string projectPath)
        {
            var cancelRunningUnity = false;
            var recoveryFile = Path.Combine(projectPath, "Temp", "__Backupscenes", "0.backup");
            if (File.Exists(recoveryFile))
            {
                var result = MessageBox.Show("Crash recovery scene found, do you want to copy it into Assets/_Recovery/-folder?", "UnityLauncherPro - Scene Recovery", MessageBoxButton.YesNo, MessageBoxImage.Question);
                if (result == MessageBoxResult.Yes)
                {
                    var restoreFolder = Path.Combine(projectPath, "Assets", "_Recovery");
                    if (Directory.Exists(restoreFolder) == false)
                    {
                        Directory.CreateDirectory(restoreFolder);
                    }
                    if (Directory.Exists(restoreFolder) == true)
                    {
                        Int32 unixTimestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
                        var uniqueFileName = "Recovered_Scene" + unixTimestamp + ".unity";
                        File.Copy(recoveryFile, Path.Combine(restoreFolder, uniqueFileName));
                        Console.WriteLine("Recovered crashed scene into: " + restoreFolder);
                    }
                    else
                    {
                        Console.WriteLine("Error: Failed to create restore folder: " + restoreFolder);
                        cancelRunningUnity = true;
                    }
                }
                else if (result == MessageBoxResult.Cancel) // dont do restore, but run Unity
                {
                    cancelRunningUnity = true;
                }
            }
            return cancelRunningUnity;
        }

        public static string GetUnityExePath(string version)
        {
            if (string.IsNullOrEmpty(version) == true) return null;
            return MainWindow.unityInstalledVersions.ContainsKey(version) ? MainWindow.unityInstalledVersions[version] : null;
        }


        // opens Explorer to target folder
        public static bool LaunchExplorer(string folder)
        {
            if (Directory.Exists(folder) == true)
            {
                Process.Start(folder);
                return true;
            }
            else // original folder is missing, try to find parent folder that we can go into
            {
                for (int i = folder.Length - 1; i > -1; i--)
                {
                    // TODO path.separator
                    if (folder[i] == '/')
                    {
                        if (Directory.Exists(folder.Substring(0, i)))
                        {
                            Process.Start(folder.Substring(0, i) + "/");
                            break;
                        }
                    }
                }
            }
            return false;
        }

        public static bool LaunchExplorerSelectFile(string fileName)
        {
            if (File.Exists(fileName) == true)
            {

                fileName = Path.GetFullPath(fileName);
                Process.Start("explorer.exe", string.Format("/select,\"{0}\"", fileName));
                return true;
            }
            else // file is missing, try to find parent folder that we can go into
            {
                for (int i = fileName.Length - 1; i > -1; i--)
                {
                    if (fileName[i] == '/')
                    {
                        if (Directory.Exists(fileName.Substring(0, i)))
                        {
                            Process.Start(fileName.Substring(0, i) + "/");
                            break;
                        }
                    }
                }
            }
            return false;
        }

        // run any exe, return process
        public static Process LaunchExe(string path, string param = null, bool captureOutput = false)
        {
            if (string.IsNullOrEmpty(path)) return null;

            // not needed for exe's in PATH
            //if (File.Exists(path) == true)
            {
                Process newProcess = null;
                if (string.IsNullOrEmpty(param) == true)
                {
                    Console.WriteLine("LaunchExe= " + path);
                    newProcess = Process.Start(path);
                }
                else
                {
                    Console.WriteLine("LaunchExe= " + path + " param=" + param);

                    try
                    {
                        newProcess = new Process();
                        newProcess.StartInfo.FileName = "\"" + path + "\"";
                        newProcess.StartInfo.Arguments = param;
                        if (captureOutput)
                        {
                            newProcess.StartInfo.RedirectStandardError = true;
                            newProcess.StartInfo.RedirectStandardOutput = true;
                            newProcess.StartInfo.UseShellExecute = false;
                        }
                        newProcess.EnableRaisingEvents = true; // needed to get Exited event
                        newProcess.Start();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);
                    }
                }
                return newProcess;
            }
            // Console.WriteLine("Failed to run exe: " + path + " " + param);
            // return null;
        }


        public static string GetUnityReleaseURL(string version)
        {
            if (string.IsNullOrEmpty(version)) return null;

            var cleanVersion = CleanVersionNumber(version);
            string url = $"https://unity.com/releases/editor/whats-new/{cleanVersion}#notes";

            //if (VersionIsArchived(version) == true)
            //{
            //    // remove f#, TODO should remove c# from china version ?
            //    version = Regex.Replace(version, @"f[0-9]{1,2}", "", RegexOptions.IgnoreCase);

            //    string padding = "unity-";
            //    string whatsnew = "whats-new";

            //    if (version.Contains("5.6")) padding = "";
            //    if (version.Contains("2018.2")) whatsnew = "whatsnew";
            //    if (version.Contains("2018.3")) padding = "";
            //    if (version.Contains("2018.1")) whatsnew = "whatsnew";
            //    if (version.Contains("2017.4.")) padding = "";
            //    if (version.Contains("2018.4.")) padding = "";

            //    // later versions seem to follow this
            //    var year = int.Parse(version.Split('.')[0]);
            //    if (year >= 2019) padding = "";

            //    url = "https://unity3d.com/unity/" + whatsnew + "/" + padding + version;
            //}
            //else
            //if (VersionIsPatch(version) == true)
            //{
            //    url = "https://unity3d.com/unity/qa/patch-releases/" + version;
            //}
            //else
            //if (VersionIsBeta(version) == true)
            //{
            //    url = "https://unity3d.com/unity/beta/" + version;
            //}
            //else
            //if (VersionIsAlpha(version) == true)
            //{
            //    url = "https://unity3d.com/unity/alpha/" + version;
            //}
            return url;
        }

        // if version contains *f* its archived version
        public static bool VersionIsArchived(string version)
        {
            return version.Contains("f");
        }

        public static bool VersionIsPatch(string version)
        {
            return version.Contains("p");
        }

        public static bool VersionIsBeta(string version)
        {
            return version.Contains("b");
        }

        public static bool VersionIsAlpha(string version)
        {
            return version.Contains("a");
        }

        public static bool VersionIsChinese(string version)
        {
            return version.Contains("c1");
        }


        //as of 21 May 2021, only final 'f' versions are now available on the alpha release notes for Unity 2018 and newer. 2017 and 5 still have patch 'p' versions as well.
        public static bool HasAlphaReleaseNotes(string version) => VersionIsArchived(version) || VersionIsPatch(version);

        public static string GetAlphaReleaseNotesURL(string fromVersion, string toVersion = null)
            => "https://alpha.release-notes.ds.unity3d.com/search?fromVersion=" + fromVersion + "&toVersion=" + (toVersion != null ? toVersion : fromVersion);

        // open release notes page in browser
        public static bool OpenReleaseNotes(string version)
        {
            bool result = false;
            if (string.IsNullOrEmpty(version)) return false;

            string url = null;
            if (Properties.Settings.Default.useAlphaReleaseNotes && HasAlphaReleaseNotes(version))
            {
                url = GetAlphaReleaseNotesURL(version);
            }
            else
            {
                url = GetUnityReleaseURL(version);
            }

            if (string.IsNullOrEmpty(url)) return false;

            OpenURL(url);
            result = true;
            return result;
        }

        public static bool OpenReleaseNotes_Cumulative(string version)
        {
            bool result = false;
            if (string.IsNullOrEmpty(version)) return false;

            string url = null;
            var comparisonVersion = version;
            //with the alpha release notes, we want a diff between an installed version and the one selected, but the site just shows all the changes inclusive of "fromVersion=vers"
            //so if we find a good installed candidate, we need the version just above it (installed or not) that has release notes page
            var closestInstalledVersion = Tools.FindNearestVersion(version, MainWindow.unityInstalledVersions.Keys.ToList(), true);
            if (closestInstalledVersion != null)
            {
                comparisonVersion = closestInstalledVersion;
                string nextFinalVersionAfterInstalled = closestInstalledVersion;

                //wwe need a loop here, to find the nearest final version. It might be better to warn the user about this before opening the page.
                do
                    nextFinalVersionAfterInstalled = Tools.FindNearestVersion(nextFinalVersionAfterInstalled, MainWindow.updatesAsStrings);
                while (nextFinalVersionAfterInstalled != null && !HasAlphaReleaseNotes(nextFinalVersionAfterInstalled));

                if (nextFinalVersionAfterInstalled != null) comparisonVersion = nextFinalVersionAfterInstalled;

            }
            url = GetAlphaReleaseNotesURL(comparisonVersion, version);

            OpenURL(url);
            result = true;
            return result;
        }

        public static void OpenURL(string url)
        {
            Process.Start(url);
        }

        public static async void DownloadInBrowser(string version, bool preferFullInstaller = false)
        {
            if (version == null) return;
            string exeURL = await GetUnityUpdates.FetchDownloadUrl(version);

            // null from unity api? then try direct download
            // https://beta.unity3d.com/download/330fbefc18b7/UnityDownloadAssistant-6000.1.0a8.exe
            if (exeURL == null)
            {
                Console.WriteLine("TODO DownloadInBrowser");
            }

            if (preferFullInstaller == true)
            {
                exeURL = exeURL.Replace("UnityDownloadAssistant-" + version + ".exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
            }

            Console.WriteLine("DownloadInBrowser exeURL= '" + exeURL + "'");

            if (string.IsNullOrEmpty(exeURL) == false && exeURL.StartsWith("https"))
            {
                //SetStatus("Download installer in browser: " + exeURL);
                Process.Start(exeURL);
            }
            else // not found
            {
                //SetStatus("Error> Cannot find installer executable ... opening website instead");
                const string url = "https://unity3d.com/get-unity/download/archive";
                Process.Start(url + "#installer-not-found-for-version-" + version);
            }
        }

        public static async void DownloadAndInstall(string version)
        {
            if (version == null)
            {
                Console.WriteLine("Error> Cannot download and install null version");
                return;
            }
            string exeURL = await GetUnityUpdates.FetchDownloadUrl(version);

            Console.WriteLine("download exeURL= (" + exeURL + ")");

            if (string.IsNullOrEmpty(exeURL) == false && exeURL.StartsWith("https") == true)
            {
                //SetStatus("Download installer in browser: " + exeURL);
                // download url file to temp
                string tempFile = Path.GetTempPath() + "UnityDownloadAssistant-" + version.Replace(".", "_") + ".exe";
                //Console.WriteLine("download tempFile= (" + tempFile + ")");
                if (File.Exists(tempFile) == true) File.Delete(tempFile);

                // TODO make async
                if (await DownloadFileAsync(exeURL, tempFile))
                {
                    // get base version, to use for install path
                    // FIXME check if have any paths?
                    string lastRootFolder = Properties.Settings.Default.rootFolders[Properties.Settings.Default.rootFolders.Count - 1];

                    // check if ends with / or \
                    if (lastRootFolder.EndsWith("/") == false && lastRootFolder.EndsWith("\\") == false) lastRootFolder += "\\";

                    string outputVersionFolder = version.Split('.')[0] + "_" + version.Split('.')[1];
                    string targetPathArgs = " /D=" + lastRootFolder + outputVersionFolder; ;

                    // if user clicks NO to UAC, this fails (so added try-catch)
                    try
                    {
                        Process process = new Process();
                        process.StartInfo.FileName = tempFile;
                        process.StartInfo.Arguments = targetPathArgs;
                        process.EnableRaisingEvents = true;
                        process.Exited += (sender, e) => DeleteTempFile(tempFile);
                        process.Start();
                    }
                    catch (Exception)
                    {
                        Console.WriteLine("Failed to run exe: " + tempFile);
                        DeleteTempFile(tempFile);
                    }
                    // TODO refresh upgrade dialog after installer finished
                }
            }
            else // not found
            {
                //SetStatus("Error> Cannot find installer executable ... opening website instead");
                var url = "https://unity3d.com/get-unity/download/archive";
                Process.Start(url + "#installer-not-found---version-" + version);
            }
        }

        static readonly string initFileDefaultURL = "https://raw.githubusercontent.com/unitycoder/UnityInitializeProject/main/Assets/Editor/InitializeProject.cs";

        public static async Task DownloadInitScript(string currentInitScriptFullPath, string currentInitScriptLocationOrURL)
        {
            string currentInitScriptFolder = Path.GetDirectoryName(currentInitScriptFullPath);
            string currentInitScriptFile = Path.GetFileName(currentInitScriptFullPath);
            string tempFile = Path.Combine(Path.GetTempPath(), currentInitScriptFile);
            bool isLocalFile = false;

            if (string.IsNullOrEmpty(currentInitScriptLocationOrURL) == true) currentInitScriptLocationOrURL = initFileDefaultURL;

            // check if its URL or local file
            if (currentInitScriptLocationOrURL.ToLower().StartsWith("http") == true)
            {
                // download into temp first
                if (await DownloadFileAsync(currentInitScriptLocationOrURL, tempFile) == false) return;
            }
            else // file is in local folders/drives/projects
            {
                // check if file exists
                if (File.Exists(currentInitScriptLocationOrURL) == false) return;

                tempFile = currentInitScriptLocationOrURL;
                isLocalFile = true;
            }

            // if got file
            if (File.Exists(tempFile) == true)
            {
                // just in case file is locked
                try
                {
                    // small validation to check if its valid editor script
                    var tempContent = File.ReadAllText(tempFile);
                    if (tempContent.IndexOf("public class InitializeProject") > 0 && tempContent.IndexOf("namespace UnityLauncherProTools") > 0 && tempContent.IndexOf("public static void Init()") > 0)
                    {
                        // create scripts folder if missing
                        if (Directory.Exists(currentInitScriptFolder) == false) Directory.CreateDirectory(currentInitScriptFolder);

                        // move old file as backup
                        if (File.Exists(currentInitScriptFullPath))
                        {
                            string oldScriptFullPath = Path.Combine(currentInitScriptFolder, currentInitScriptFile + ".bak");
                            if (File.Exists(oldScriptFullPath)) File.Delete(oldScriptFullPath);
                            File.Move(currentInitScriptFullPath, oldScriptFullPath);
                        }
                        // move new file here (need to delete old to overwrite)
                        if (File.Exists(currentInitScriptFullPath)) File.Delete(currentInitScriptFullPath);

                        // local file copy, not move
                        if (isLocalFile == true)
                        {
                            File.Copy(tempFile, currentInitScriptFullPath);
                        }
                        else
                        {
                            File.Move(tempFile, currentInitScriptFullPath);
                        }
                    }
                    else
                    {
                        Console.WriteLine("Invalid c# init file..(missing correct Namespace, Class or Method)");
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine("File exception: " + e.Message);
                }
            }
            else
            {
                Console.WriteLine("Failed to download init script from: " + currentInitScriptLocationOrURL);
            }
        }

        static void DeleteTempFile(string path)
        {
            if (File.Exists(path) == true)
            {
                Console.WriteLine("DeleteTempFile: " + path);
                File.Delete(path);
            }
        }

        public static string DownloadHTML(string url)
        {
            Console.WriteLine("DownloadHTML: " + url);

            if (string.IsNullOrEmpty(url) == true) return null;
            using (WebClient client = new WebClient())
            {
                try
                {
                    // download page html
                    return client.DownloadString(url);
                }
                catch (WebException e)
                {
                    Console.WriteLine("DownloadHTML: " + e.Message);
                    return null;
                }
            }
        }

        public static string CleanVersionNumber(string version)
        {
            if (string.IsNullOrEmpty(version)) return null;

            var split = version.Split('.');
            float parsedVersion = float.Parse($"{split[0]}.{split[1]}");
            // 2023.3 and newer Alpha releases, no replace
            if (IsAlpha(version) && parsedVersion >= 2023.3)
            {
                // do nothing
            }
            else
            {
                // note old patch versions still contains p## in the end
                version = Regex.Replace(version, @"[f|a|b][0-9]{1,2}", "", RegexOptions.IgnoreCase);
            }
            return version;
        }

        // TODO only hash version is used, cleanup the rest
        public static string ParseDownloadURLFromWebpage(string version, string hash = null, bool preferFullInstaller = false, bool useHash = false)
        {
            string exeURL = "";

            //Console.WriteLine("ParseDownloadURLFromWebpage: " + version + ", hash: " + useHash);

            if (string.IsNullOrEmpty(version)) return null;

            // NOTE no longer uses f# in the end

            string url = null;
            if (useHash == false)
            {
                var cleanVersion = CleanVersionNumber(version);
                // NOTE 2024 June, installs are now located on separate pages, like https://unity.com/releases/editor/whats-new/6000.0.5#installs

                // get correct page url
                //url = "https://unity3d.com/get-unity/download/archive";
                // fix unity server problem, some pages says 404 found if no url params
                url = "https://unity.com/releases/editor/whats-new/" + cleanVersion + "?unitylauncherpro#installs";
                //if (VersionIsPatch(version)) url = "https://unity3d.com/unity/qa/patch-releases";
                if (VersionIsBeta(version)) url = "https://unity.com/releases/editor/beta/" + version;
                if (VersionIsAlpha(version)) url = "https://unity.com/releases/editor/alpha/" + version;
                //url += "?unitylauncherpro";
            }
            else
            {
                // NOTE version here is actually VERSION|HASH
                //string hash = version;
                url = $"https://beta.unity3d.com/download/{hash}/download.html";

                //Console.WriteLine("hashurl: " + url);

                //version = FetchUnityVersionNumberFromHTML(url);
                //Console.WriteLine(url);
                //Console.WriteLine("got "+version);
                if (string.IsNullOrEmpty(version))
                {
                    SetStatus("Failed to get version (" + version + ") number from hash: " + hash);
                    return null;
                }
            }

            //Console.WriteLine("scanning installers from url: " + url);

            //string sourceHTML = DownloadHTML(url);

            //if (string.IsNullOrEmpty(sourceHTML) == true)
            //{
            //    Console.WriteLine("Failed to download html from: " + url);
            //    return null;
            //}

            //// parse changeset hash from html
            //string pattern = $@"href=""unityhub://{version}/([^""]+)""";
            //Regex regex = new Regex(pattern);
            //Match match = regex.Match(sourceHTML);

            //if (match.Success == true)
            //{
            //    string changeSet = match.Groups[1].Value;
            //    Console.WriteLine("changeSet: " + changeSet);
            //}

            exeURL = $"https://beta.unity3d.com/download/{hash}/UnityDownloadAssistant-{version}.exe";
            //string[] lines = sourceHTML.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);

            // patch version download assistant finder
            //if (useHash == false && VersionIsPatch(version))
            //{
            //    for (int i = 0; i < lines.Length; i++)
            //    {
            //        //if (lines[i].Contains("UnityDownloadAssistant-" + version + ".exe"))
            //        if (lines[i].Contains("UnitySetup64-" + version + ".exe"))
            //        {
            //            int start = lines[i].IndexOf('"') + 1;
            //            int end = lines[i].IndexOf('"', start);
            //            exeURL = lines[i].Substring(start, end - start);
            //            break;
            //        }
            //    }
            //}
            //else if (useHash == false && VersionIsArchived(version))
            //{
            //    // archived version download assistant finder
            //    for (int i = 0; i < lines.Length; i++)
            //    {
            //        // find line where full installer is (from archive page)
            //        if (lines[i].Contains("UnitySetup64-" + version))
            //        {

            //            Console.WriteLine(lines[i]);

            //            // take full exe installer line, to have changeset hash, then replace with download assistant filepath
            //            string line = lines[i];
            //            int start = line.IndexOf('"') + 1;
            //            int end = line.IndexOf('"', start);
            //            exeURL = line.Substring(start, end - start);
            //            exeURL = exeURL.Replace("Windows64EditorInstaller/UnitySetup64-", "UnityDownloadAssistant-");
            //            break;
            //        }
            //    }
            //}
            //else // alpha or beta version download assistant finder
            //{
            //    // regular beta
            //    // 
            //    // https://beta.unity3d.com/download/21aeb48b6ed2/UnityDownloadAssistant.exe

            //    // hidden beta
            //    // 
            //    // https://beta.unity3d.com/download/8008bc0c1b74/UnityDownloadAssistant-6000.0.0b15.exe

            //    // new 10.06.2024, no more downloadassistant.exe in html

            //    // check html lines
            //    for (int i = 0; i < lines.Length; i++)
            //    {
            //        //Console.WriteLine(lines[i]);
            //        //if (lines[i].Contains("UnityDownloadAssistant"))
            //        if (lines[i].Contains("UnityDownloadAssistant"))
            //        {
            //            if (useHash == false)
            //            {
            //                string pattern = @"https://beta\.unity3d\.com/download/[a-zA-Z0-9]+/UnityDownloadAssistant\.exe";
            //                Match match = Regex.Match(lines[i], pattern);
            //                if (match.Success)
            //                {
            //                    exeURL = match.Value;
            //                }
            //                else
            //                {
            //                    Console.WriteLine("No match found for download base url..");
            //                }
            //            }
            //            else // hidden download page
            //            {
            //                string pattern = @"UnityDownloadAssistant(?:-\d+\.\d+\.\d+[bf]\d*)?\.exe";
            //                Match match = Regex.Match(lines[i], pattern);
            //                if (match.Success)
            //                {
            //                    // append base url
            //                    Regex regex = new Regex(@"(https://beta\.unity3d\.com/download/[a-zA-Z0-9]+/)");
            //                    Match match2 = regex.Match(url);

            //                    //Console.WriteLine("source url: " + url);

            //                    if (match2.Success)
            //                    {
            //                        string capturedUrl = match2.Groups[1].Value;
            //                        exeURL = capturedUrl + match.Value;
            //                    }
            //                    else
            //                    {
            //                        Console.WriteLine("No match found for download base url..");
            //                    }
            //                }
            //                break;
            //            }
            //        }
            //    } // for lines
            //} // alpha or beta

            // download full installer instead, TODO probably not needed anymore?
            if (useHash == false && preferFullInstaller == true)
            {
                exeURL = exeURL.Replace("UnityDownloadAssistant-" + version + ".exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
                // handle alpha/beta
                exeURL = exeURL.Replace("UnityDownloadAssistant.exe", "Windows64EditorInstaller/UnitySetup64-" + version + ".exe");
            }

            // didnt find installer
            if (string.IsNullOrEmpty(exeURL))
            {
                //SetStatus("Cannot find UnityDownloadAssistant.exe for this version.");
                Console.WriteLine("Installer not found from URL: " + url);
            }
            return exeURL;
        }

        private static string FetchUnityVersionNumberFromHTML(string url)
        {
            string sourceHTML = DownloadHTML(url);

            if (string.IsNullOrEmpty(sourceHTML)) return null;

            string pattern = @"\d+\.\d+\.\d+[bf]\d+";
            MatchCollection matches = Regex.Matches(sourceHTML, pattern);
            if (matches.Count > 0)
            {
                foreach (Match match in matches)
                {
                    Console.WriteLine("Extracted number: " + match.Value);
                    return match.Value;
                    //break;
                }
            }
            else
            {
                Console.WriteLine("FetchUnityVersionNumberFromHTML: No match found.");
            }

            return null;

            //if (string.IsNullOrEmpty(sourceHTML) == false)
            //{
            //    // find version number from html
            //    string pattern = @"UnityDownloadAssistant-[0-9]+\.[0-9]+\.[0-9]+[a-z]?\.exe";
            //    Match match = Regex.Match(sourceHTML, pattern);
            //    if (match.Success)
            //    {
            //        version = match.Value.Replace("UnityDownloadAssistant-", "").Replace(".exe", "");
            //    }
            //}
            //return version;
        }

        public static string FindNearestVersion(string currentVersion, List allAvailable, bool checkBelow = false)
        {
            if (allAvailable == null)
                return null;

            string result = null;

            // add current version to list, to sort it with others
            if (!allAvailable.Contains(currentVersion))
                allAvailable.Add(currentVersion);

            // sort list
            if (checkBelow)
            {
                allAvailable.Sort((s1, s2) => VersionAsLong(s1).CompareTo(VersionAsLong(s2)));
            }
            else
            {
                allAvailable.Sort((s1, s2) => VersionAsLong(s2).CompareTo(VersionAsLong(s1)));
            }

            // check version above our current version
            int currentIndex = allAvailable.IndexOf(currentVersion);
            // if its index 0, we select that row anyways later
            if (currentIndex > 0 && currentIndex < allAvailable.Count)
            {
                result = allAvailable[currentIndex - 1];
            }

            return result;
        }

        // returns version as integer, for easier sorting between versions: 2019.4.19f1 = 2019041901
        public static long VersionAsLong(string version)
        {
            long result = 0;

            // cleanup 32bit version name, TODO is this needed anymore?
            string cleanVersion = version.Replace("(32-bit)", "");

            // remove a (alpha),b (beta),f (final?),p (path),c (china final)
            cleanVersion = cleanVersion.Replace("a", ".1.");
            cleanVersion = cleanVersion.Replace("b", ".2.");
            cleanVersion = cleanVersion.Replace("c", ".3."); // NOTE this was 'c1'
            cleanVersion = cleanVersion.Replace("f", ".4.");
            cleanVersion = cleanVersion.Replace("p", ".5.");

            // split values
            string[] splitted = cleanVersion.Split('.');
            if (splitted.Length > 1)
            {
                long multiplier = 1;
                for (long i = 0, length = splitted.Length; i < length; i++)
                {
                    long n = int.Parse(splitted[length - 1 - i]);
                    result += n * multiplier;
                    multiplier *= 50;
                }
            }
            return result;
        }

        // https://stackoverflow.com/a/1619103/5452781
        public static KeyValuePair GetEntry(this IDictionary dictionary, TKey key)
        {
            return new KeyValuePair(key, dictionary[key]);
        }

        public static void HandleDataGridScrollKeys(object sender, KeyEventArgs e)
        {
            /*
            DataGrid grid = sender as DataGrid;
            switch (e.Key)
            {
                case Key.Up:
                    if (grid.SelectedIndex > 0)
                    {
                        grid.SelectedIndex--;
                    }
                    // disable wrap around

                    //else
                    //{
                     //   grid.SelectedIndex = grid.Items.Count - 1;
                    //}
            e.Handled = true;
            break;
                case Key.Down:
                    if (grid.SelectedIndex < grid.Items.Count)
            {
                grid.SelectedIndex++;
            }
            //grid.SelectedIndex = ++grid.SelectedIndex % grid.Items.Count;
            e.Handled = true;
            break;
        }
        grid.ScrollIntoView(grid.Items[grid.SelectedIndex]);
        */
        }

        // NOTE this doesnt modify the 2nd line in ProjectVersion.txt
        static void SaveProjectVersion(Project proj)
        {
            var settingsPath = Path.Combine(proj.Path, "ProjectSettings", "ProjectVersion.txt");
            if (File.Exists(settingsPath))
            {
                var versionRows = File.ReadAllLines(settingsPath);
                versionRows[0] = "m_EditorVersion: " + proj.Version;
                File.WriteAllLines(settingsPath, versionRows);
            }
        }

        public static void DisplayUpgradeDialog(Project proj, MainWindow owner, bool useInitScript = false)
        {
            UpgradeWindow modalWindow = new UpgradeWindow(proj.Version, proj.Path, proj.Arguments);
            modalWindow.ShowInTaskbar = owner == null;
            modalWindow.WindowStartupLocation = owner == null ? WindowStartupLocation.CenterScreen : WindowStartupLocation.CenterOwner;
            modalWindow.Topmost = owner == null;
            modalWindow.ShowActivated = true;
            modalWindow.Owner = owner;
            modalWindow.ShowDialog();
            var results = modalWindow.DialogResult.HasValue && modalWindow.DialogResult.Value;

            if (results == true)
            {
                var upgradeToVersion = UpgradeWindow.upgradeVersion;
                if (string.IsNullOrEmpty(upgradeToVersion)) return;

                // get selected version to upgrade for
                Console.WriteLine("Upgrade to " + upgradeToVersion);

                // inject new version for this item, TODO inject version to ProjectSettings file, so then no alert from unity wrong version dialog
                proj.Version = upgradeToVersion;
                SaveProjectVersion(proj);
                var proc = LaunchProject(proj, dataGridRef: null, useInitScript: false, upgrade: true);

                // TODO update datagrid row for new version
            }
            else
            {
                //Console.WriteLine("results = " + results);
            }
        }

        /// 
        /// install context menu item to registry
        /// 
        /// 
        public static void AddContextMenuRegistry(string contextRegRoot)
        {
            RegistryKey key = Registry.CurrentUser.OpenSubKey(contextRegRoot, true);

            // add folder if missing
            if (key == null)
            {
                key = Registry.CurrentUser.CreateSubKey(@"Software\Classes\Directory\Background\Shell");
            }

            if (key != null)
            {
                var appName = "UnityLauncherPro";
                key.CreateSubKey(appName);

                key = key.OpenSubKey(appName, true);
                key.SetValue("", "Open with " + appName);
                key.SetValue("Icon", "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"");

                key.CreateSubKey("command");
                key = key.OpenSubKey("command", true);
                var executeString = "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"";
                executeString += " -projectPath \"%V\"";
                key.SetValue("", executeString);
            }
            else
            {
                Console.WriteLine("Error> Cannot find registry key: " + contextRegRoot);
            }
        }

        /// 
        /// uninstall context menu item from registry
        /// 
        /// 
        public static void RemoveContextMenuRegistry(string contextRegRoot)
        {
            RegistryKey key = Registry.CurrentUser.OpenSubKey(contextRegRoot, true);
            if (key != null)
            {
                var appName = "UnityLauncherPro";
                RegistryKey appKey = Registry.CurrentUser.OpenSubKey(contextRegRoot + "\\" + appName, false);
                if (appKey != null)
                {
                    key.DeleteSubKeyTree(appName);
                    //SetStatus("Removed context menu registry items");
                }
                else
                {
                    //SetStatus("Nothing to uninstall..");
                }
            }
            else
            {
                //SetStatus("Error> Cannot find registry key: " + contextRegRoot);
            }
        }

        /// 
        /// reads .git/HEAD file from the project to get current branch name
        /// 
        /// 
        /// 
        public static string ReadGitBranchInfo(string projectPath, bool searchParentFolders)
        {
            string results = null;

            if (searchParentFolders)
            {
                DirectoryInfo directoryInfo = new DirectoryInfo(projectPath);

                while (directoryInfo != null)
                {
                    string dirName = Path.Combine(directoryInfo.FullName, ".git");

                    if (Directory.Exists(dirName))
                    {
                        string branchFile = Path.Combine(dirName, "HEAD");
                        if (File.Exists(branchFile))
                        {
                            // removes extra end of line
                            results = string.Join(" ", File.ReadAllLines(branchFile));
                            // get branch only
                            int pos = results.LastIndexOf("/") + 1;
                            results = results.Substring(pos, results.Length - pos);
                            return results;
                        }
                    }
                    directoryInfo = directoryInfo.Parent;
                }
            }
            else
            {
                string dirName = Path.Combine(projectPath, ".git");
                if (Directory.Exists(dirName))


                {
                    string branchFile = Path.Combine(dirName, "HEAD");
                    if (File.Exists(branchFile))

                    {
                        // removes extra end of line
                        results = string.Join(" ", File.ReadAllLines(branchFile));
                        // get branch only
                        int pos = results.LastIndexOf("/") + 1;
                        results = results.Substring(pos, results.Length - pos);

                    }

                }
            }
            return results;
        }

        public static string ReadPlasticBranchInfo(string projectPath)
        {
            string results = null;
            string dirName = Path.Combine(projectPath, ".plastic");
            if (Directory.Exists(dirName))
            {
                string branchFile = Path.Combine(dirName, "plastic.selector");
                if (File.Exists(branchFile))
                {
                    // removes extra end of line
                    results = string.Join(" ", File.ReadAllText(branchFile));
                    // get branch only
                    int pos = results.LastIndexOf("\"/") + 1;
                    // -1 to remove last "
                    results = results.Substring(pos, results.Length - pos - 1);
                }
            }
            return results;
        }

        //public static Platform GetTargetPlatform(string projectPath)
        static string GetTargetPlatformRaw(string projectPath)
        {
            string results = null;
            //Platform results = Platform.Unknown;

            // get buildtarget from .csproj
            // StandaloneWindows64:19
            // get main csproj file
            var csproj = Path.Combine(projectPath, "Assembly-CSharp.csproj");
            // TODO check projname also, if no assembly-.., NOTE already checked above
            //var csproj = Path.Combine(projectPath, projectName + ".csproj");
            if (File.Exists(csproj))
            {
                var csprojtxt = File.ReadAllText(csproj);
                var csprojsplit = csprojtxt.Split(new[] { "" }, StringSplitOptions.None);
                if (csprojsplit != null && csprojsplit.Length > 1)
                {
                    var endrow = csprojsplit[1].IndexOf(":");
                    if (endrow > -1)
                    {
                        //Console.WriteLine("build target: " + csprojsplit[1].Substring(0, endrow));
                        // 5.6 : win32, win64, osx, linux, linux64, ios, android, web, webstreamed, webgl, xboxone, ps4, psp2, wsaplayer, tizen, samsungtv
                        // 2017: standalone, Win, Win64, OSXUniversal, Linux, Linux64, LinuxUniversal, iOS, Android, Web, WebStreamed, WebGL, XboxOne, PS4, PSP2, WindowsStoreApps, Switch, WiiU, N3DS, tvOS, PSM
                        // 2018: standalone, Win, Win64, OSXUniversal, Linux, Linux64, LinuxUniversal, iOS, Android, Web, WebStreamed, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, N3DS, tvOS
                        // 2019: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
                        // 2020: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
                        // 2021: Standalone, Win, Win64, OSXUniversal, Linux64, iOS, Android, WebGL, XboxOne, PS4, WindowsStoreApps, Switch, tvOS
                        results = csprojsplit[1].Substring(0, endrow);
                        //results = (Platform)Enum.Parse(typeof(Platform), csprojsplit[1].Substring(0, endrow));
                    }
                }
            }
            else
            {
                //Console.WriteLine("Missing csproj, cannot parse target platform: "+ projectPath);
            }

            return results;
        }

        public static string GetTargetPlatform(string projectPath)
        {
            var rawPlatformName = GetTargetPlatformRaw(projectPath);

            if (string.IsNullOrEmpty(rawPlatformName) == false && GetProjects.remapPlatformNames.ContainsKey(rawPlatformName))
            {
                return GetProjects.remapPlatformNames[rawPlatformName];
            }
            else
            {
                if (string.IsNullOrEmpty(rawPlatformName) == false) Console.WriteLine("Missing buildTarget remap name for: " + rawPlatformName);
                return null;
            }
        }

        public static string ReadCustomProjectData(string projectPath, string customFile)
        {
            string results = null;
            customFile = Path.Combine(projectPath, "ProjectSettings", customFile);
            if (File.Exists(customFile) == true)
            {
                results = string.Join(" ", File.ReadAllLines(customFile));
            }
            return results;
        }

        public static bool SaveCustomProjectData(string projectPath, string customFile, string data)
        {
            customFile = Path.Combine(projectPath, "ProjectSettings", customFile);

            try
            {
                File.WriteAllText(customFile, data);
                return true;
            }
            catch (Exception)
            {
            }

            return false;
        }

        public static bool HasFocus(DependencyObject obj, Control control, bool checkChildren)
        {
            var oFocused = FocusManager.GetFocusedElement(obj) as DependencyObject;
            if (!checkChildren)
                return oFocused == control;
            while (oFocused != null)
            {
                if (oFocused == control)
                    return true;
                oFocused = VisualTreeHelper.GetParent(oFocused);
            }
            return false;
        }

        public static void SetFocusToGrid(DataGrid targetGrid, int index = -1)
        {
            // set main component focus
            //targetGrid.Focus();
            //Keyboard.Focus(targetGrid);

            // no items
            if (targetGrid.Items.Count < 1) return;

            // keep current row selected
            if (index == -1 && targetGrid.SelectedIndex > -1) index = targetGrid.SelectedIndex;

            // if no item selected, pick first
            if (index == -1) index = 0;

            targetGrid.SelectedIndex = index;

            // set full focus
            DataGridRow row = (DataGridRow)targetGrid.ItemContainerGenerator.ContainerFromIndex(index);
            if (row == null)
            {
                targetGrid.UpdateLayout();
                if (index < targetGrid.Items.Count)
                {
                    // scroll selected into view
                    targetGrid.ScrollIntoView(targetGrid.Items[index]);
                    row = (DataGridRow)targetGrid.ItemContainerGenerator.ContainerFromIndex(index);
                }
                else
                {
                    Console.WriteLine("selected row out of bounds: " + index);
                }
            }
            // NOTE does this causes move below?
            //row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            if (row != null)
            {
                row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Up)); // works better than Up
                row.Focus();
                Keyboard.Focus(row);
            }
        }

        public static string BrowseForOutputFolder(string title, string initialDirectory = null)
        {
            // https://stackoverflow.com/a/50261723/5452781
            // Create a "Save As" dialog for selecting a directory (HACK)
            var dialog = new SaveFileDialog();
            if (initialDirectory != null) dialog.InitialDirectory = initialDirectory;
            dialog.Title = title;
            dialog.Filter = "Project Folder|*.Folder"; // Prevents displaying files
            dialog.FileName = "Project"; // Filename will then be "select.this.directory"
            if (dialog.ShowDialog() == true)
            {
                string path = dialog.FileName;
                // Remove fake filename from resulting path
                path = path.Replace("\\Project.Folder", "");
                path = path.Replace("Project.Folder", "");
                // If user has changed the filename, create the new directory
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
                return path;
            }
            return null;
        }

        // TODO too many params..
        public static Project FastCreateProject(string version, string baseFolder, string projectName = null, string templateZipPath = null, string[] platformsForThisUnity = null, string platform = null, bool useInitScript = false, string initScriptPath = null)
        {
            // check for base folders in settings tab
            if (string.IsNullOrEmpty(baseFolder) == true)
            {
                Console.WriteLine("Missing baseFolder value");
                return null;
            }

            // check if base folder exists
            if (Directory.Exists(baseFolder) == false)
            {
                Console.WriteLine("Missing baseFolder: " + baseFolder);
                return null;
            }

            // check selected unity version
            if (string.IsNullOrEmpty(version) == true)
            {
                Console.WriteLine("Missing unity version string");
                return null;
            }

            string newPath = null;
            // if we didnt have name yet
            if (string.IsNullOrEmpty(projectName) == true)
            {
                projectName = GetSuggestedProjectName(version, baseFolder);
                // failed getting new path a-z
                if (projectName == null) return null;
            }
            newPath = Path.Combine(baseFolder, projectName);

            // create folder
            CreateEmptyProjectFolder(newPath, version);

            // unzip template, if any
            if (templateZipPath != null)
            {
                TarLib.Tar.ExtractTarGz(templateZipPath, newPath);
            }

            // copy init file into project
            if (useInitScript == true)
            {
                if (File.Exists(initScriptPath) == true)
                {
                    var editorTargetFolder = Path.Combine(baseFolder, projectName, "Assets", "Editor");
                    if (Directory.Exists(editorTargetFolder) == false) Directory.CreateDirectory(editorTargetFolder);
                    var targetScriptFile = Path.Combine(editorTargetFolder, Path.GetFileName(initScriptPath));
                    // TODO overwrite old file, there shouldnt be anything here
                    if (File.Exists(targetScriptFile) == false) File.Copy(initScriptPath, targetScriptFile);
                }
            }

            // launch empty project
            var proj = new Project();
            proj.Title = projectName;
            proj.Path = Path.Combine(baseFolder, newPath).Replace("\\", "/");
            proj.Version = version;
            proj.TargetPlatforms = platformsForThisUnity;
            proj.TargetPlatform = platform;
            proj.Modified = DateTime.Now;
            proj.folderExists = true; // have to set this value, so item is green on list

            var proc = LaunchProject(proj, null, useInitScript);
            ProcessHandler.Add(proj, proc);

            return proj;
        } // FastCreateProject


        public static string GetSuggestedProjectName(string version, string baseFolder)
        {
            // check for base folders in settings tab, could use currently selected project folder parent as base?
            if (string.IsNullOrEmpty(baseFolder))
            {
                Console.WriteLine("Missing txtRootFolderForNewProjects");
                return null;
            }

            // find next free folder checking all "unityversion_a-z" characters
            var unityBaseVersion = version.Substring(0, version.LastIndexOf('.'));
            unityBaseVersion = unityBaseVersion.Replace(".", "_");
            for (int i = 97; i < 122; i++)
            {
                var newProject = unityBaseVersion + "_" + ((char)i);
                var path = Path.Combine(baseFolder, newProject);

                if (Directory.Exists(path))
                {
                    //Console.WriteLine("directory exists..trying again");
                }
                else // its available
                {
                    return newProject;
                }
            }
            // couldnt find free letter to use, lets add timestamp then
            return unityBaseVersion + "_" + DateTime.Now.ToString("ddMMyyyy_HHmmss");
        }

        static void CreateEmptyProjectFolder(string path, string version)
        {
            Console.WriteLine("Create new project folder: " + path);
            Directory.CreateDirectory(path);

            // create project version file, to avoid wrong version warning
            var settingsPath = Path.Combine(path, "ProjectSettings");
            Directory.CreateDirectory(settingsPath);
            var settingsFile = Path.Combine(settingsPath, "ProjectVersion.txt");
            File.WriteAllText(settingsFile, "m_EditorVersion: " + version);
        }

        public static string GetEditorLogsFolder()
        {
            return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Unity", "Editor");
        }

        public static string[] GetPlatformsForUnityVersion(string version)
        {
            // get platforms array for this unity version
            // TODO use dictionary instead of looping versions
            for (int i = 0; i < MainWindow.unityInstallationsSource.Count; i++)
            {
                if (MainWindow.unityInstallationsSource[i].Version == version)
                {
                    return MainWindow.unityInstallationsSource[i].Platforms;
                }
            }
            return null;
        }

        // https://stackoverflow.com/a/675347/5452781
        public static void SetStartupRegistry(bool state)
        {
            RegistryKey rk = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
            if (state == true)
            {
                rk.SetValue(MainWindow.appName, "\"" + Process.GetCurrentProcess().MainModule.FileName + "\"");
            }
            else
            {
                rk.DeleteValue(MainWindow.appName, false);
            }
        }

        public static Dictionary ScanTemplates(string unityInstallPath)
        {
            var items = new Dictionary();

            // add none as default item
            items.Add("None", null);

            // get list of existing packages
            var unityPath = Path.GetDirectoryName(unityInstallPath);
            var templateFolder = Path.Combine(unityPath, "Data/Resources/PackageManager/ProjectTemplates/");
            if (Directory.Exists(templateFolder) == false) return items;

            var fileEntries = Directory.GetFiles(templateFolder).ToList();

            // process found files
            for (int i = fileEntries.Count - 1; i > -1; i--)
            {
                // check if its tgz
                if (fileEntries[i].IndexOf(".tgz") == -1)
                {
                    fileEntries.RemoveAt(i);
                }
                else
                {
                    // cleanup name
                    var name = Path.GetFileName(fileEntries[i]).Replace("com.unity.template.", "").Replace(".tgz", "");
                    items.Add(name, fileEntries[i]);
                }
            }

            return items;
        }

        // chatgpt
        public static string GetElapsedTime(DateTime datetime)
        {
            TimeSpan ts = DateTime.Now - datetime;

            if (ts.TotalSeconds < 60)
            {
                return ts.TotalSeconds < 2 ? "Right now" : $"{(int)ts.TotalSeconds} seconds ago";
            }
            else if (ts.TotalMinutes < 60)
            {
                return ts.TotalMinutes < 2 ? "1 minute ago" : $"{(int)ts.TotalMinutes} minutes ago";
            }
            else if (ts.TotalHours < 24)
            {
                return ts.TotalHours < 2 ? "1 hour ago" : $"{(int)ts.TotalHours} hours ago";
            }
            else if (ts.TotalDays < 30)
            {
                return ts.TotalDays < 2 ? "1 day ago" : $"{(int)ts.TotalDays} days ago";
            }
            else if (ts.TotalDays < 365)
            {
                if (ts.TotalDays < 60)
                {
                    return "1 month ago";
                }
                else
                {
                    return $"{(int)(ts.TotalDays / 30)} months ago";
                }
            }
            else
            {
                return ts.TotalDays < 730 ? "1 year ago" : $"{(int)(ts.TotalDays / 365)} years ago";
            }
        }

        public static bool ValidateDateFormat(string format)
        {
            try
            {
                String formattedDate = DateTime.Now.ToString(format);
                DateTime.Parse(formattedDate);
                return true;
            }
            catch (Exception)
            {
                //Console.WriteLine("Invalid custom datetime format: " + format);
                return false;
            }
        }

        // https://stackoverflow.com/a/37724335/5452781
        public static void BringProcessToFront(Process process)
        {
            IntPtr handle = process.MainWindowHandle;
            if (IsIconic(handle))
            {
                ShowWindow(handle, SW_RESTORE);
            }

            SetForegroundWindow(handle);
        }

        public static void DownloadAdditionalModules(string UnityExePath, string unityVersion, string moduleName)
        {
            var editorFolder = Path.GetDirectoryName(UnityExePath);

            string hash = null;

            // get from unity exe (only for 2018.4 and later?)
            var versionInfo = FileVersionInfo.GetVersionInfo(UnityExePath);
            var versionRaw = versionInfo.ProductVersion.Split('_');
            if (versionRaw.Length == 2)
            {
                hash = versionRaw[1];
            }
            else // try other files
            {
                var changeSetFile = Path.Combine(editorFolder, @"Data\PlaybackEngines\windowsstandalonesupport\Source\WindowsPlayer\WindowsPlayer\UnityConfigureRevision.gen.h");
                if (File.Exists(changeSetFile) == true)
                {
                    var allText = File.ReadAllText(changeSetFile);
                    var hashRaw = allText.Split(new string[] { "#define UNITY_VERSION_HASH \"" }, StringSplitOptions.None);
                    if (hashRaw.Length > 1)
                    {
                        hash = hashRaw[1].Replace("\"", "");
                    }
                    else
                    {
                        Console.WriteLine("Unable to parse UNITY_VERSION_HASH from " + changeSetFile);
                    }
                }
                else
                {
                    Console.WriteLine("Changeset hash file not found: " + changeSetFile);
                }
            }

            if (hash == null) return;

            var moduleURL = "https://download.unity3d.com/download_unity/" + hash + "/TargetSupportInstaller/UnitySetup-" + moduleName + "-Support-for-Editor-" + unityVersion + ".exe";
            OpenURL(moduleURL);
        }

        public static void OpenAppdataSpecialFolder(string subfolder)
        {
            var logfolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), subfolder);
            if (Directory.Exists(logfolder) == true)
            {
                if (Tools.LaunchExplorer(logfolder) == false)
                {
                    Console.WriteLine("Cannot open folder.." + logfolder);
                }
            }
        }

        // NOTE android only at the moment
        public static void BuildProject(Project proj, Platform platform)
        {
            Console.WriteLine("Building " + proj.Title + " for " + platform);
            SetStatus("Build process started: " + DateTime.Now.ToString("HH:mm:ss"));

            // TODO use theme colors, keep list of multiple builds, if click status button show list of builds, if click for single build (show output folder)
            SetBuildStatus(Colors.Red);

            if (string.IsNullOrEmpty(proj.Path)) return;

            // create builder script template (with template string, that can be replaced with project related paths or names?)
            // copy editor build script to Assets/Editor/ folder (if already exists then what? Use UnityLauncherBuildSomething.cs name, so can overwrite..)
            var editorScriptFolder = Path.Combine(proj.Path, "Assets", "Editor");
            if (Directory.Exists(editorScriptFolder) == false) Directory.CreateDirectory(editorScriptFolder);
            // TODO check if creation failed

            // create output file for editor script
            var editorScriptFile = Path.Combine(editorScriptFolder, "UnityLauncherProBuilder.cs");

            // check build folder and create if missing
            var outputFolder = Path.Combine(proj.Path, "Builds/" + platform + "/");
            outputFolder = outputFolder.Replace('\\', '/'); // fix backslashes
            Console.WriteLine("outputFolder= " + outputFolder);
            if (Directory.Exists(outputFolder) == false) Directory.CreateDirectory(outputFolder);
            // TODO check if creation failed

            // cleanup filename from project name
            var invalidChars = Path.GetInvalidFileNameChars();
            var outputFile = String.Join("_", proj.Title.Split(invalidChars, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.');
            // replace spaces also, for old time(r)s
            outputFile = outputFile.Replace(' ', '_');
            outputFile = Path.Combine(outputFolder, outputFile + ".apk");
            Console.WriteLine("outputFile= " + outputFile);

            // TODO move to txt resource? and later load from local custom file if exists, and later open window or add settings for build options
            // TODO different unity versions? wont work in older unitys right now
            var builderScript = @"using System.Linq;
using UnityEditor;
using UnityEngine;
public static class UnityLauncherProTools
{
    public static void BuildAndroid()
    {
        EditorUserBuildSettings.buildAppBundle = false;
        EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle;
        PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
        PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64;
        var settings = new BuildPlayerOptions();
        settings.scenes = GetScenes();
        settings.locationPathName = ""###OUTPUTFILE###"";
        settings.target = BuildTarget.Android;
        settings.options = BuildOptions.None;
        var report = BuildPipeline.BuildPlayer(settings);
    }
    public static void BuildiOS() // Note need to match platform name
    {
        PlayerSettings.iOS.targetDevice = iOSTargetDevice.iPhoneAndiPad;
        var settings = new BuildPlayerOptions();
        settings.scenes = GetScenes();
        settings.locationPathName = ""###OUTPUTFOLDER###"";
        settings.target = BuildTarget.iOS;
        settings.options = BuildOptions.None;
        var report = BuildPipeline.BuildPlayer(settings);
    }
    static string[] GetScenes()
    {
        return EditorBuildSettings.scenes.Where(scene => scene.enabled).Select(scene => scene.path).ToArray();
    }
}";

            // fill in project specific data
            builderScript = builderScript.Replace("###OUTPUTFILE###", outputFile); // android
            builderScript = builderScript.Replace("###OUTPUTFOLDER###", outputFolder); // ios
            Console.WriteLine("builderScript=" + builderScript);

            File.WriteAllText(editorScriptFile, builderScript);
            // TODO check if write failed

            // get selected project unity exe path
            var unityExePath = Tools.GetUnityExePath(proj.Version);
            if (unityExePath == null) return;

            // create commandline string for building and launch it
            //var buildcmd = $"\"{unityExePath}\" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"Builder.BuildAndroid\" -buildTarget android -logFile -";
            // TODO test without nographics : https://forum.unity.com/threads/batch-build-one-scene-is-black-works-in-normal-file-build.1282823/#post-9456524
            var buildParams = $" -quit -batchmode -nographics -projectPath \"{proj.Path}\" -executeMethod \"UnityLauncherProTools.Build{platform}\" -buildTarget {platform} -logFile \"{outputFolder}/../build.log\"";
            Console.WriteLine("buildcmd= " + buildParams);

            // launch build
            var proc = Tools.LaunchExe(unityExePath, buildParams);

            // wait for process exit then open output folder
            proc.Exited += (o, i) =>
            {
                Console.WriteLine("Build process exited: " + outputFolder);
                Tools.ExploreFolder(outputFolder);
                SetStatus("Build process finished: " + DateTime.Now.ToString("HH:mm:ss"));
                // TODO set color based on results
                SetBuildStatus(Colors.Green);
            };

        }

        // runs unity SimpleWebServer.exe and launches default Browser into project build/ folder'
        public static void LaunchWebGL(Project proj, string relativeFolder)
        {
            var projPath = proj?.Path.Replace('/', '\\');
            if (string.IsNullOrEmpty(projPath) == true) return;

            var buildPath = Path.Combine(projPath, "Builds", relativeFolder);
            if (Directory.Exists(buildPath) == false) return;

            if (MainWindow.unityInstalledVersions.ContainsKey(proj.Version) == false) return;

            // get mono and server exe paths
            var editorPath = Path.GetDirectoryName(MainWindow.unityInstalledVersions[proj.Version]);

            var monoToolsPath = Path.Combine(editorPath, "Data/MonoBleedingEdge/bin");
            if (Directory.Exists(monoToolsPath) == false) return;

            var webglToolsPath = Path.Combine(editorPath, "Data/PlaybackEngines/WebGLSupport/BuildTools");
            if (Directory.Exists(webglToolsPath) == false) return;

            var monoExe = Path.Combine(monoToolsPath, "mono.exe");
            if (File.Exists(monoExe) == false) return;

            var webExe = Path.Combine(webglToolsPath, "SimpleWebServer.exe");
            if (File.Exists(webExe) == false) return;

            int port = MainWindow.webglPort;
            if (port < 50000) port = 50000;
            if (port > 65534) port = 65534;

            // check if this project already has server running and process is not closed
            if (webglServerProcesses.ContainsKey(port) && webglServerProcesses[port].HasExited == false)
            {
                Console.WriteLine("Port found in cache: " + port + " process=" + webglServerProcesses[port]);

                // check if project matches
                if (webglServerProcesses[port].StartInfo.Arguments.IndexOf("\"" + buildPath + "\"") > -1)
                {
                    Console.WriteLine("this project already has webgl server running.. lets open browser url only");
                    // then open browser url only
                    Tools.OpenURL("http://localhost:" + port);
                    return;

                }
                else
                {
                    Console.WriteLine("Port in use, but its different project: " + port);
                    Console.WriteLine(webglServerProcesses[port].StartInfo.Arguments + " == " + "\"" + buildPath + "\"");

                    // then open new port and process
                    // -----------------------------------------------------------
                    // check if port is available https://stackoverflow.com/a/2793289
                    bool isAvailable = true;
                    IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
                    IPEndPoint[] objEndPoints = ipGlobalProperties.GetActiveTcpListeners();

                    // NOTE instead of iterating all ports, just try to open port, if fails, open next one
                    // compare with existing ports, if available
                    for (int i = 0; i < objEndPoints.Length; i++)
                    {
                        if (objEndPoints[i].Port == port)
                        {
                            port++;
                            if (port > 65534)
                            {
                                Console.WriteLine("Failed to find open port..");
                                isAvailable = false;
                                return;
                            }
                        }
                    }

                    Console.WriteLine("Found available port: " + port);

                    if (isAvailable == false)
                    {
                        Console.WriteLine("failed to open port " + port + " (should be open already, or something else is using it?)");
                    }
                    else
                    {
                        // take process id from unity, if have it (then webserver closes automatically when unity is closed)
                        var proc = ProcessHandler.Get(proj.Path);
                        int pid = proc == null ? -1 : proc.Id;
                        string param = null;

                        // parse proj version year as number 2019.4.1f1 -> 2019
                        int year = 0;
                        var versionParts = proj.Version.Split('.');
                        bool parsedYear = int.TryParse(versionParts[0], out year);

                        if (parsedYear && year >= 6000)
                        {
                            param = "\"" + webExe + "\" \"" + buildPath + "\" " + "http://localhost:" + port + "/" + (pid == -1 ? "" : " " + pid);
                        }
                        else // older versions or failed to parse
                        {
                            param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port
                        }

                        var webglServerProcess = Tools.LaunchExe(monoExe, param);

                        if (webglServerProcesses.ContainsKey(port))
                        {
                            Console.WriteLine("Error> Should not happen - this port is already in dictionary! port: " + port);
                        }
                        else // keep reference to this process on this port
                        {
                            // TODO how to remove process once its closed? (or unlikely to have many processes in total? can also remove during check, if process already null)
                            webglServerProcesses.Add(port, webglServerProcess);
                            Console.WriteLine("Added port " + port);
                        }

                        Tools.OpenURL("http://localhost:" + port);
                    }
                    // -----------------------------------------------------------

                }
            }
            else
            {
                Console.WriteLine("Port not running in cache or process already closed, remove it from cache: " + port);
                if (webglServerProcesses.ContainsKey(port)) webglServerProcesses.Remove(port);

                // TODO remove duplicate code
                // then open new process
                // -----------------------------------------------------------
                // check if port is available https://stackoverflow.com/a/2793289
                bool isAvailable = true;
                IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
                IPEndPoint[] objEndPoints = ipGlobalProperties.GetActiveTcpListeners();

                // compare with existing ports, if available
                for (int i = 0; i < objEndPoints.Length; i++)
                {
                    if (objEndPoints[i].Port == port)
                    {
                        if (port > 65535)
                        {
                            Console.WriteLine("Failed to find open port..");
                            isAvailable = false;
                            return;
                        }
                        port++;
                    }
                }

                Console.WriteLine("Found available port: " + port);

                if (isAvailable == false)
                {
                    Console.WriteLine("failed to open port " + port + " (should be open already, or something else is using it?)");
                }
                else
                {
                    // take process id from unity, if have it(then webserver closes automatically when unity is closed)
                    var proc = ProcessHandler.Get(proj.Path);
                    int pid = proc == null ? -1 : proc.Id;

                    // parse proj version year as number 2019.4.1f1 -> 2019
                    string param = null;
                    int year = 0;
                    var versionParts = proj.Version.Split('.');
                    bool parsedYear = int.TryParse(versionParts[0], out year);

                    if (parsedYear && year >= 6000)
                    {
                        param = "\"" + webExe + "\" \"" + buildPath + "\" " + "\"http://localhost:" + port + "/\"" + (pid == -1 ? "" : " " + pid);
                    }
                    else // older versions or failed to parse
                    {
                        param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port
                    }

                    //var param = "\"" + webExe + "\" \"" + buildPath + "\" " + port + (pid == -1 ? "" : " " + pid); // server exe path, build folder and port

                    var webglServerProcess = Tools.LaunchExe(monoExe, param);

                    if (webglServerProcess == null)
                    {
                        Console.WriteLine("Failed to start exe..");
                    }

                    if (webglServerProcesses.ContainsKey(port))
                    {
                        Console.WriteLine("Error> Should not happen - this port is already in dictionary! port: " + port);
                    }
                    else // keep reference to this process on this port
                    {
                        // TODO how to remove process once its closed? (or unlikely to have many processes in total? can also remove during check, if process already null)
                        webglServerProcesses.Add(port, webglServerProcess);
                        Console.WriteLine("Added port " + port);
                    }

                    Tools.OpenURL("http://localhost:" + port);
                }
                // -----------------------------------------------------------

            }
        } // LaunchWebGL()

        // creates .bat file to launch UnityLauncherPro and then .url link file on desktop, into that .bat file
        public static bool CreateDesktopShortCut(Project proj, string batchFolder)
        {
            string lnkFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop));

            if (string.IsNullOrEmpty(batchFolder)) return false;

            //string batchFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "UnityLauncherPro");
            if (Directory.Exists(batchFolder) == false) Directory.CreateDirectory(batchFolder);
            string batFileName = Path.Combine(batchFolder, proj.Title + ".bat");
            string launcherExe = Process.GetCurrentProcess().MainModule.FileName;
            string args = "-projectPath " + "\"" + proj.Path + "\" " + proj.Arguments;
            string description = "Unity Project: " + proj.Title;

            // create .bat file
            var batLauncherData = "start \"\" \"" + launcherExe + "\"" + " " + args;
            File.WriteAllText(batFileName, batLauncherData);

            // create desktop link file
            using (StreamWriter writer = new StreamWriter(lnkFileName + "\\" + proj.Title + ".url"))
            {
                writer.WriteLine("[InternetShortcut]");
                writer.WriteLine("URL=file:///" + batFileName);
                //writer.WriteLine("ShowCommand=7"); // doesnt work for minimized
                writer.WriteLine("IconIndex=0");
                writer.WriteLine("Arguments=-projectPath " + proj.Path);
                // TODO maybe could take icon from project (but then need to convert into .ico)
                string iconExe = GetUnityExePath(proj.Version);
                if (iconExe == null) iconExe = launcherExe;
                string icon = iconExe.Replace('\\', '/');
                writer.WriteLine("IconFile=" + icon);
            }

            // TODO check for streamwriter and file write success

            return true;
        }


        internal static long GetFolderSizeInBytes(string currentBuildReportProjectPath)
        {
            // FIXME: 0 is not really correct for missing folder..
            if (Directory.Exists(currentBuildReportProjectPath) == false) return 0;

            return DirSize(new DirectoryInfo(currentBuildReportProjectPath));
        }

        // https://stackoverflow.com/a/468131/5452781
        static long DirSize(DirectoryInfo d)
        {
            long size = 0;
            // Add file sizes.
            FileInfo[] fis = d.GetFiles();
            foreach (FileInfo fi in fis)
            {
                size += fi.Length;
            }
            // Add subdirectory sizes.
            DirectoryInfo[] dis = d.GetDirectories();
            foreach (DirectoryInfo di in dis)
            {
                size += DirSize(di);
            }
            return size;
        }

        // Returns the human-readable file size for an arbitrary, 64-bit file size 
        // The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB"
        internal static string GetBytesReadable(long i)
        {
            // Get absolute value
            long absolute_i = (i < 0 ? -i : i);
            // Determine the suffix and readable value
            string suffix;
            double readable;
            if (absolute_i >= 0x1000000000000000) // Exabyte
            {
                suffix = "EB";
                readable = (i >> 50);
            }
            else if (absolute_i >= 0x4000000000000) // Petabyte
            {
                suffix = "PB";
                readable = (i >> 40);
            }
            else if (absolute_i >= 0x10000000000) // Terabyte
            {
                suffix = "TB";
                readable = (i >> 30);
            }
            else if (absolute_i >= 0x40000000) // Gigabyte
            {
                suffix = "GB";
                readable = (i >> 20);
            }
            else if (absolute_i >= 0x100000) // Megabyte
            {
                suffix = "MB";
                readable = (i >> 10);
            }
            else if (absolute_i >= 0x400) // Kilobyte
            {
                suffix = "KB";
                readable = i;
            }
            else
            {
                return i.ToString("0 B"); // Byte
            }
            // Divide by 1024 to get fractional value
            readable = (readable / 1024);
            // Return formatted number with suffix
            return readable.ToString("0.### ") + suffix;
        }

        public static MainWindow mainWindow;

        // set status bar in main thread
        public static void SetStatus(string text)
        {
            mainWindow.Dispatcher.Invoke(() => { mainWindow.SetStatus(text); });
        }

        public static void SetBuildStatus(Color color)
        {
            mainWindow.Dispatcher.Invoke(() => { mainWindow.SetBuildStatus(color); });
        }

        // https://unity3d.com/unity/alpha
        public static bool IsAlpha(string version)
        {
            if (string.IsNullOrEmpty(version)) return false;
            return version.IndexOf("a", 0, StringComparison.CurrentCultureIgnoreCase) > -1;
        }

        // https://unity3d.com/beta/
        public static bool IsBeta(string version)
        {
            if (string.IsNullOrEmpty(version)) return false;
            return version.IndexOf("b", 0, StringComparison.CurrentCultureIgnoreCase) > -1;
        }

        // https://unity3d.com/unity/qa/lts-releases
        public static bool IsLTS(string versionRaw)
        {
            if (string.IsNullOrEmpty(versionRaw)) return false;
            var version = versionRaw.Split('.');
            var versionInt = int.Parse(version[0]);
            var versionMinor = int.Parse(version[1]);
            return (versionInt >= 2017 && versionMinor == 4) || (versionInt > 2019 && versionMinor == 3);
        }

        internal static void UninstallEditor(string path, string version)
        {
            if (string.IsNullOrEmpty(path)) return;
            if (string.IsNullOrEmpty(version)) return;

            // run uninstaller from path
            var installFolder = Path.GetDirectoryName(path);
            var uninstaller = Path.Combine(installFolder, "Uninstall.exe");
            // TODO could be optional setting for non-silent uninstall
            LaunchExe(uninstaller, "/S");
            // remove firewall settings
            var cmd = "netsh advfirewall firewall delete rule name=all program=\"" + path + "\"";
            Console.WriteLine("Cleanup firewall: " + cmd);
            LaunchExe("cmd.exe", "/c " + cmd);

            int year;
            string[] parts = version.Split('.');
            // TODO handle unity 6.x
            if (parts.Length >= 1 && int.TryParse(parts[0], out year) && year  cancellationTokenSource.Cancel());
            progressWindow.Show();
            var result = false;
            try
            {
                using (var client = new HttpClient())
                using (var response = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token))
                {
                    response.EnsureSuccessStatusCode();

                    var totalBytes = response.Content.Headers.ContentLength ?? 1;
                    var buffer = new byte[8192];
                    var totalRead = 0;

                    using (var contentStream = await response.Content.ReadAsStreamAsync())
                    using (var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write,
                               FileShare.None, buffer.Length, true))
                    {
                        int bytesRead;
                        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, cancellationTokenSource.Token)) > 0)
                        {
                            await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationTokenSource.Token);
                            totalRead += bytesRead;
                            progressWindow.UpdateProgress(new DownloadProgress(totalRead, totalBytes));
                        }
                        result = true;
                    }
                }
            }
            catch (TaskCanceledException)
            {
                Console.WriteLine("Download cancelled");
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                if (!result)
                {
                    DeleteTempFile(destinationPath);
                }
                progressWindow.Close();
            }
            return result;
        }

        internal static string GetSRP(string projectPath)
        {
            // read projectsettings/graphicsettings file, look for m_SRPDefaultSettings: value
            var settingsFile = Path.Combine(projectPath, "ProjectSettings", "GraphicsSettings.asset");
            if (File.Exists(settingsFile) == false) return null;

            var allText = File.ReadAllText(settingsFile);
            var srpIndex = allText.IndexOf("m_SRPDefaultSettings:");
            if (srpIndex == -1) return null; // BIRP

            // urp = UnityEngine.Rendering.Universal.UniversalRenderPipeline
            // hdrp = UnityEngine.Rendering.HighDefinition.HDRenderPipeline

            if (allText.IndexOf("UnityEngine.Rendering.Universal.UniversalRenderPipeline", srpIndex) > -1)
            {
                return "URP";
            }
            else if (allText.IndexOf("UnityEngine.Rendering.HighDefinition.HDRenderPipeline", srpIndex) > -1)
            {
                return "HDRP";
            }
            else
            {
                return null; // BIRP
            }

        }
    } // class

} // namespace

Web Proxy Viewer  |  New URL  |  Original Page