diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3dbac44..973993f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: shell: powershell run: | # Get the commit message - $strVal = '${{ github.event.commits[0].message }}' + $strVal ='${{ github.event. head_commit.message }}' # Convert commit message to a single line if multiline $singleLineStrVal = $strVal -replace "`r`n", " " -replace "`n", " " if ($singleLineStrVal -match '#GITBUILD') { @@ -167,8 +167,11 @@ jobs: if: env.build_trigger == 'true' shell: cmd run: | + setlocal EnableDelayedExpansion echo === Running BuildInstaller.bat === call .\BuildInstaller.bat || echo WARNING: BuildInstaller.bat exited with %ERRORLEVEL%, continuing... + set "ec=!ERRORLEVEL!" + if not "!ec!"=="0" echo WARNING: BuildInstaller.bat exited with !ec!, continuing... echo === Verifying MSI === if exist "UnityLauncherProInstaller\Release\UnityLauncherPro-Installer.msi" ( echo Success: MSI found at UnityLauncherProInstaller\Release\UnityLauncherPro-Installer.msi diff --git a/README.md b/README.md index f7540b8..b519e02 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,4 @@ Old (winforms) version is here: https://github.com/unitycoder/UnityLauncher + diff --git a/UnityLauncherPro/App.xaml b/UnityLauncherPro/App.xaml index 1d882de..6aedd47 100644 --- a/UnityLauncherPro/App.xaml +++ b/UnityLauncherPro/App.xaml @@ -136,7 +136,7 @@ - + @@ -257,6 +257,7 @@ + @@ -530,9 +531,6 @@ - - - diff --git a/UnityLauncherPro/Controls/SearchBoxControl.xaml b/UnityLauncherPro/Controls/SearchBoxControl.xaml new file mode 100644 index 0000000..c9d0fb6 --- /dev/null +++ b/UnityLauncherPro/Controls/SearchBoxControl.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/UnityLauncherPro/Controls/SearchBoxControl.xaml.cs b/UnityLauncherPro/Controls/SearchBoxControl.xaml.cs new file mode 100644 index 0000000..97ab6d4 --- /dev/null +++ b/UnityLauncherPro/Controls/SearchBoxControl.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace UnityLauncherPro.Controls +{ + public partial class SearchBoxControl : UserControl + { + public event TextChangedEventHandler SearchTextChanged; + public event RoutedEventHandler SearchCleared; + + public event KeyEventHandler SearchKeyDown; + + public SearchBoxControl() + { + InitializeComponent(); + } + + public string SearchText + { + get { return txtSearchBox.Text; } + set { txtSearchBox.Text = value; } + } + + public new void Focus() + { + txtSearchBox.Focus(); + txtSearchBox.Select(txtSearchBox.Text.Length, 0); + } + + public void Clear() + { + txtSearchBox.Text = ""; + } + + private void TxtSearchBox_PreviewKeyDown(object sender, KeyEventArgs e) + { + SearchKeyDown?.Invoke(this, e); + } + + private void TxtSearchBox_TextChanged(object sender, TextChangedEventArgs e) + { + SearchTextChanged?.Invoke(this, e); + } + + private void OnClearSearchClick(object sender, RoutedEventArgs e) + { + Clear(); + SearchCleared?.Invoke(this, e); + } + } +} diff --git a/UnityLauncherPro/Converters/ThumbnailConverter.cs b/UnityLauncherPro/Converters/ThumbnailConverter.cs new file mode 100644 index 0000000..955194b --- /dev/null +++ b/UnityLauncherPro/Converters/ThumbnailConverter.cs @@ -0,0 +1,83 @@ +using System; +using System.Globalization; +using System.IO; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media.Imaging; + +namespace UnityLauncherPro.Converters +{ + public class ThumbnailConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + // Return UnsetValue if no project is selected + if (value == null) + { + return DependencyProperty.UnsetValue; + } + + if (value is Project project) + { + if (!string.IsNullOrEmpty(project.Path)) + { + string thumbnailPath = Path.Combine(project.Path, "ProjectSettings", "icon.png"); + + if (File.Exists(thumbnailPath)) + { + try + { + // Check if this is for Width/Height parameter + if (parameter != null && (parameter.ToString() == "Width" || parameter.ToString() == "Height")) + { + return 64.0; // Return default dimension + } + + // For Source binding, load the bitmap + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache; + bitmap.UriSource = new Uri(thumbnailPath, UriKind.Absolute); + bitmap.DecodePixelWidth = 64; // Match your display size + bitmap.DecodePixelHeight = 64; + + bitmap.EndInit(); + + // Freeze for cross-thread access + if (bitmap.CanFreeze) + { + bitmap.Freeze(); + } + + return bitmap; + } + catch + { + // Ignore and fall back to UnsetValue for Source, or 64.0 for dimensions + if (parameter != null && (parameter.ToString() == "Width" || parameter.ToString() == "Height")) + { + return 1.0; + } + return DependencyProperty.UnsetValue; + } + } + } + + // Project path doesn't exist or no thumbnail found + if (parameter != null && (parameter.ToString() == "Width" || parameter.ToString() == "Height")) + { + return 1.0; // Return default dimension + } + return DependencyProperty.UnsetValue; + } + + return DependencyProperty.UnsetValue; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/UnityLauncherPro/Data/OnlineTemplateItem.cs b/UnityLauncherPro/Data/OnlineTemplateItem.cs new file mode 100644 index 0000000..078f3dc --- /dev/null +++ b/UnityLauncherPro/Data/OnlineTemplateItem.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; + +namespace UnityLauncherPro.Data +{ + public class OnlineTemplateItem : INotifyPropertyChanged + { + private bool _isDownloaded; + + public string Name { get; set; } + public string Description { get; set; } + public string RenderPipeline { get; set; } + public string Type { get; set; } // Core, Learning, Sample, + public string PreviewImageURL { get; set; } + public string TarBallURL { get; set; } + + public bool IsDownloaded + { + get { return _isDownloaded; } + set + { + if (_isDownloaded != value) + { + _isDownloaded = value; + OnPropertyChanged(nameof(IsDownloaded)); + } + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/UnityLauncherPro/Data/TemplateGraphQLResponse.cs b/UnityLauncherPro/Data/TemplateGraphQLResponse.cs new file mode 100644 index 0000000..32bdd91 --- /dev/null +++ b/UnityLauncherPro/Data/TemplateGraphQLResponse.cs @@ -0,0 +1,49 @@ +namespace UnityLauncherPro.Data +{ + public class TemplateGraphQLResponse + { + public TemplateData data { get; set; } + } + + public class TemplateData + { + public GetTemplates getTemplates { get; set; } + } + + public class GetTemplates + { + public TemplateEdge[] edges { get; set; } + } + + public class TemplateEdge + { + public TemplateNode node { get; set; } + } + + public class TemplateNode + { + public string name { get; set; } + public string packageName { get; set; } + public string description { get; set; } + public string type { get; set; } + public string renderPipeline { get; set; } + public PreviewImage previewImage { get; set; } + public TemplateVersion[] versions { get; set; } + } + + public class PreviewImage + { + public string url { get; set; } + } + + public class TemplateVersion + { + public string name { get; set; } + public Tarball tarball { get; set; } + } + + public class Tarball + { + public string url { get; set; } + } +} \ No newline at end of file diff --git a/UnityLauncherPro/GetUnityInstallations.cs b/UnityLauncherPro/GetUnityInstallations.cs index 7f4570d..28a0a31 100644 --- a/UnityLauncherPro/GetUnityInstallations.cs +++ b/UnityLauncherPro/GetUnityInstallations.cs @@ -48,7 +48,23 @@ public static List Scan() var haveUninstaller = File.Exists(uninstallExe); var exePath = Path.Combine(editorFolder, "Unity.exe"); - if (File.Exists(exePath) == false) continue; + +// bool supportTuanjie = true; + if (File.Exists(exePath) == false) + { + //if (supportTuanjie == false) + { + continue; + } + //else + //{ + // exePath = Path.Combine(editorFolder, "Tuanjie.exe"); + // if (File.Exists(exePath) == false) + // { + // continue; + // } + //} + } // get full version number from uninstaller (or try exe, if no uninstaller) var version = Tools.GetFileVersionData(haveUninstaller ? uninstallExe : exePath); diff --git a/UnityLauncherPro/Libraries/ExtractTarGz.cs b/UnityLauncherPro/Libraries/ExtractTarGz.cs index 04517d1..e55e44a 100644 --- a/UnityLauncherPro/Libraries/ExtractTarGz.cs +++ b/UnityLauncherPro/Libraries/ExtractTarGz.cs @@ -1,6 +1,3 @@ -// source https://gist.github.com/Su-s/438be493ae692318c73e30367cbc5c2a -// updated source https://gist.github.com/Matheos96/da8990030dfe3e27b0a48722042d9c0b - using System; using System.IO; using System.IO.Compression; @@ -11,10 +8,8 @@ namespace TarLib public class Tar { /// - /// Extracts a .tar.gz archive to the specified directory. + /// Extracts a .tar.gz archive to the specified directory. /// - /// The .tar.gz to decompress and extract. - /// Output directory to write the files. public static void ExtractTarGz(string filename, string outputDir) { using (var stream = File.OpenRead(filename)) @@ -24,39 +19,29 @@ public static void ExtractTarGz(string filename, string outputDir) } /// - /// Extracts a .tar.gz archive stream to the specified directory. + /// Extracts a .tar.gz archive stream to the specified directory. /// - /// The .tar.gz to decompress and extract. - /// Output directory to write the files. public static void ExtractTarGz(Stream stream, string outputDir) { - int read; - const int chunk = 4096; + const int chunk = 4096*4; var buffer = new byte[chunk]; - // A GZipStream is not seekable, so copy it first to a MemoryStream using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress)) + using (var memStream = new MemoryStream()) { - using (var memStream = new MemoryStream()) + int read; + while ((read = gzipStream.Read(buffer, 0, buffer.Length)) > 0) { - //For .NET 6+ - while ((read = gzipStream.Read(buffer, 0, buffer.Length)) > 0) - { - memStream.Write(buffer, 0, read); - } - memStream.Seek(0, SeekOrigin.Begin); - - //ExtractTar(gzip, outputDir); - ExtractTar(memStream, outputDir); + memStream.Write(buffer, 0, read); } + memStream.Seek(0, SeekOrigin.Begin); + ExtractTar(memStream, outputDir); } } /// - /// Extractes a tar archive to the specified directory. + /// Extracts a tar archive file. /// - /// The .tar to extract. - /// Output directory to write the files. public static void ExtractTar(string filename, string outputDir) { using (var stream = File.OpenRead(filename)) @@ -66,85 +51,206 @@ public static void ExtractTar(string filename, string outputDir) } /// - /// Extractes a tar archive to the specified directory. + /// Extracts a tar archive stream. + /// Fixes path loss caused by ignoring the POSIX 'prefix' field and wrong header offsets. /// - /// The .tar to extract. - /// Output directory to write the files. public static void ExtractTar(Stream stream, string outputDir) { - var buffer = new byte[100]; - var longFileName = string.Empty; + // Tar header constants + const int HeaderSize = 512; + byte[] header = new byte[HeaderSize]; + + string pendingLongName = null; // For GNU long name ('L') entries + while (true) { - stream.Read(buffer, 0, 100); - string name = string.IsNullOrEmpty(longFileName) ? Encoding.ASCII.GetString(buffer).Trim('\0') : longFileName; //Use longFileName if we have one read - - if (String.IsNullOrWhiteSpace(name)) break; - stream.Seek(24, SeekOrigin.Current); - stream.Read(buffer, 0, 12); - var size = Convert.ToInt64(Encoding.UTF8.GetString(buffer, 0, 12).Trim('\0').Trim(), 8); - stream.Seek(20, SeekOrigin.Current); //Move head to typeTag byte - var typeTag = stream.ReadByte(); - stream.Seek(355L, SeekOrigin.Current); //Move head to beginning of data (byte 512) - - if (typeTag == 'L') + int bytesRead = ReadExact(stream, header, 0, HeaderSize); + if (bytesRead == 0) break; // End of stream + if (bytesRead < HeaderSize) throw new EndOfStreamException("Unexpected end of tar stream."); + + // Detect two consecutive zero blocks (end of archive) + bool allZero = IsAllZero(header); + if (allZero) + { + // Peek next block; if also zero -> end + bytesRead = ReadExact(stream, header, 0, HeaderSize); + if (bytesRead == 0 || IsAllZero(header)) break; + if (bytesRead < HeaderSize) throw new EndOfStreamException("Unexpected end of tar stream."); + } + + // Parse fields (POSIX ustar) + string name = GetString(header, 0, 100); + string mode = GetString(header, 100, 8); + string uid = GetString(header, 108, 8); + string gid = GetString(header, 116, 8); + string sizeOctal = GetString(header, 124, 12); + string mtime = GetString(header, 136, 12); + string checksum = GetString(header, 148, 8); + char typeFlag = (char)header[156]; + string linkName = GetString(header, 157, 100); + string magic = GetString(header, 257, 6); // "ustar\0" or "ustar " + string version = GetString(header, 263, 2); + string uname = GetString(header, 265, 32); + string gname = GetString(header, 297, 32); + string prefix = GetString(header, 345, 155); + + // Compose full name using prefix (if present and not using GNU long name override) + if (!string.IsNullOrEmpty(prefix)) + { + name = prefix + "/" + name; + } + + // If we previously read a GNU long name block, override current name + if (!string.IsNullOrEmpty(pendingLongName)) { - //If Type Tag is 'L' we have a filename that is longer than the 100 bytes reserved for it in the header. - //We read it here and save it temporarily as it will be the file name of the next block where the actual data is - var buf = new byte[size]; - stream.Read(buf, 0, buf.Length); - longFileName = Encoding.ASCII.GetString(buf).Trim('\0'); + name = pendingLongName; + pendingLongName = null; } - else + + long size = ParseOctal(sizeOctal); + + // Handle GNU long name extension block: the data of this entry is the filename of next entry. + if (typeFlag == 'L') { - longFileName = string.Empty; //Reset longFileName if current entry is not indicating one + byte[] longNameData = new byte[size]; + ReadExact(stream, longNameData, 0, (int)size); + pendingLongName = Encoding.ASCII.GetString(longNameData).Trim('\0', '\r', '\n'); + SkipPadding(stream, size); + continue; // Move to next header + } + + // Skip PAX extended header (type 'x') - metadata only + if (typeFlag == 'x') + { + SkipData(stream, size); + SkipPadding(stream, size); + continue; + } + + // Normalize name + if (string.IsNullOrWhiteSpace(name)) continue; - var output = Path.Combine(outputDir, name); + // Directory? + bool isDirectory = typeFlag == '5' || name.EndsWith("/"); - // only include these folders - var include = (output.IndexOf("package/ProjectData~/Assets/") > -1); - include |= (output.IndexOf("package/ProjectData~/ProjectSettings/") > -1); - include |= (output.IndexOf("package/ProjectData~/Packages/") > -1); + // Inclusion filter (original logic) + string originalName = name; + bool include = + originalName.IndexOf("package/ProjectData~/Assets/", StringComparison.Ordinal) > -1 || + originalName.IndexOf("package/ProjectData~/ProjectSettings/", StringComparison.Ordinal) > -1 || + originalName.IndexOf("package/ProjectData~/Library/", StringComparison.Ordinal) > -1 || + originalName.IndexOf("package/ProjectData~/Packages/", StringComparison.Ordinal) > -1; - // rename output path from "package/ProjectData~/Assets/" into "Assets/" - output = output.Replace("package/ProjectData~/", ""); + // Strip leading prefix. + string cleanedName = originalName.StartsWith("package/ProjectData~/", StringComparison.Ordinal) + ? originalName.Substring("package/ProjectData~/".Length) + : originalName; + + string finalPath = Path.Combine(outputDir, cleanedName.Replace('/', Path.DirectorySeparatorChar)); + + if (isDirectory) + { + if (include && !Directory.Exists(finalPath)) + Directory.CreateDirectory(finalPath); + // No data to read for directory; continue to next header + SkipData(stream, size); // size should be 0 + SkipPadding(stream, size); + continue; + } + + // Ensure directory exists + if (include) + { + string dir = Path.GetDirectoryName(finalPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + } - if (include == true && !Directory.Exists(Path.GetDirectoryName(output))) Directory.CreateDirectory(Path.GetDirectoryName(output)); + // Read file data (always advance stream even if not included) + byte[] fileData = new byte[size]; + ReadExact(stream, fileData, 0, (int)size); - // not folder - //if (name.Equals("./", StringComparison.InvariantCulture) == false) - if (name.EndsWith("/") == false) //Directories are zero size and don't need anything written + if (include) + { + using (var fs = File.Open(finalPath, FileMode.Create, FileAccess.Write)) { - if (include == true) - { - //Console.WriteLine("output=" + output); - using (var str = File.Open(output, FileMode.OpenOrCreate, FileAccess.ReadWrite)) - { - var buf = new byte[size]; - stream.Read(buf, 0, buf.Length); - // take only data from this folder - str.Write(buf, 0, buf.Length); - } - } - else - { - var buf = new byte[size]; - stream.Read(buf, 0, buf.Length); - } + fs.Write(fileData, 0, fileData.Length); } } - //Move head to next 512 byte block - var pos = stream.Position; - var offset = 512 - (pos % 512); - if (offset == 512) offset = 0; + // Skip padding to 512 boundary + SkipPadding(stream, size); + } + } - stream.Seek(offset, SeekOrigin.Current); + private static string GetString(byte[] buffer, int offset, int length) + { + var s = Encoding.ASCII.GetString(buffer, offset, length); + int nullIndex = s.IndexOf('\0'); + if (nullIndex >= 0) s = s.Substring(0, nullIndex); + return s.Trim(); + } + + private static long ParseOctal(string s) + { + s = s.Trim(); + if (string.IsNullOrEmpty(s)) return 0; + try + { + return Convert.ToInt64(s, 8); + } + catch + { + // Fallback: treat as decimal if malformed + long val; + return long.TryParse(s, out val) ? val : 0; } } - } // class Tar -} // namespace TarLib + private static bool IsAllZero(byte[] buffer) + { + for (int i = 0; i < buffer.Length; i++) + if (buffer[i] != 0) return false; + return true; + } + + private static int ReadExact(Stream stream, byte[] buffer, int offset, int count) + { + int total = 0; + while (total < count) + { + int read = stream.Read(buffer, offset + total, count - total); + if (read <= 0) break; + total += read; + } + return total; + } + + private static void SkipData(Stream stream, long size) + { + if (size <= 0) return; + const int chunk = 8192; + byte[] tmp = new byte[Math.Min(chunk, (int)size)]; + long remaining = size; + while (remaining > 0) + { + int toRead = (int)Math.Min(tmp.Length, remaining); + int read = stream.Read(tmp, 0, toRead); + if (read <= 0) throw new EndOfStreamException("Unexpected end while skipping data."); + remaining -= read; + } + } + + private static void SkipPadding(Stream stream, long size) + { + long padding = (512 - (size % 512)) % 512; + if (padding > 0) + { + stream.Seek(padding, SeekOrigin.Current); + } + } + } +} /* This software is available under 2 licenses-- choose whichever you prefer. @@ -184,4 +290,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ +*/ \ No newline at end of file diff --git a/UnityLauncherPro/MainWindow.xaml b/UnityLauncherPro/MainWindow.xaml index 1e19791..5e350ec 100644 --- a/UnityLauncherPro/MainWindow.xaml +++ b/UnityLauncherPro/MainWindow.xaml @@ -5,6 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:UnityLauncherPro" xmlns:System="clr-namespace:System;assembly=mscorlib" + xmlns:controls="clr-namespace:UnityLauncherPro.Controls" xmlns:converters="clr-namespace:UnityLauncherPro.Converters" x:Class="UnityLauncherPro.MainWindow" mc:Ignorable="d" Title="UnityLauncherPro" Height="670" Width="880" WindowStartupLocation="CenterScreen" Background="{DynamicResource ThemeDarkestBackground}" MinWidth="780" MinHeight="650" AllowsTransparency="True" WindowStyle="None" Margin="0" KeyDown="OnWindowKeyDown" Closing="Window_Closing" SizeChanged="Window_SizeChanged" Icon="Images/icon.ico" SourceInitialized="Window_SourceInitialized" MouseDown="Window_MouseDown"> @@ -12,6 +13,7 @@ + @@ -49,44 +51,10 @@ - - - - - - - - - - - - - + - - + - - + @@ -519,7 +404,7 @@ - - - - - @@ -611,28 +496,28 @@ @@ -641,10 +526,10 @@ @@ -652,7 +537,7 @@ @@ -663,48 +548,16 @@ + @@ -816,7 +669,7 @@ - @@ -835,8 +688,8 @@ - @@ -866,9 +719,9 @@ - diff --git a/UnityLauncherPro/MainWindow.xaml.cs b/UnityLauncherPro/MainWindow.xaml.cs index e965383..5988bb2 100644 --- a/UnityLauncherPro/MainWindow.xaml.cs +++ b/UnityLauncherPro/MainWindow.xaml.cs @@ -197,6 +197,19 @@ void Start() if (chkFetchAdditionalInfo.IsChecked == true) Tools.FetchAdditionalInfoForEditors(); + // Subscribe to search box events + searchBoxProjects.SearchTextChanged += SearchBoxProjects_SearchTextChanged; + searchBoxProjects.SearchKeyDown += SearchBoxProjects_SearchKeyDown; + + searchBoxUnitys.SearchTextChanged += searchBoxUnitys_SearchTextChanged; + searchBoxUnitys.SearchKeyDown += searchBoxUnitys_SearchKeyDown; + + searchBoxUpdates.SearchTextChanged += searchBoxUpdates_SearchTextChanged; + searchBoxUpdates.SearchKeyDown += searchBoxUpdates_SearchKeyDown; + + searchBoxBuildReport.SearchTextChanged += searchBoxBuildReport_SearchTextChanged; + searchBoxBuildReport.SearchKeyDown += searchBoxBuildReport_SearchKeyDown; + isInitializing = false; } // Start() @@ -333,7 +346,17 @@ void HandleCommandLineLaunch() { bool useInitScript = (bool)chkUseInitScript.IsChecked; - Tools.DisplayUpgradeDialog(proj, null, useInitScript); + // if not editors found, then dont open commandline? + if (unityInstallationsSource.Count > 0) + { + Tools.DisplayUpgradeDialog(proj, null, useInitScript); + } + else + { + MessageBox.Show("No Unity installations found. Please setup Unity Editor root folders first by running UnityLauncherPro.", "No Unity Installations found", MessageBoxButton.OK, MessageBoxImage.Warning); + // TODO display setup tab + } + } else // no Assets folder here OR Assets folder is empty, then its new project { @@ -398,7 +421,7 @@ private bool LaunchProjectViaPipe(Project proj) void FilterRecentProjects() { // https://www.wpftutorial.net/DataViews.html - _filterString = txtSearchBox.Text; + _filterString = searchBoxProjects.SearchText; if (_filterString.IndexOf(' ') > -1) { @@ -422,7 +445,7 @@ void FilterRecentProjects() void FilterUpdates() { - _filterString = txtSearchBoxUpdates.Text.Trim(); + _filterString = searchBoxUpdates.SearchText.Trim(); ICollectionView collection = CollectionViewSource.GetDefaultView(dataGridUpdates.ItemsSource); if (collection == null) return; @@ -435,7 +458,7 @@ void FilterUpdates() void FilterUnitys() { - _filterString = txtSearchBoxUnity.Text.Trim(); + _filterString = searchBoxUnitys.SearchText.Trim(); ICollectionView collection = CollectionViewSource.GetDefaultView(dataGridUnitys.ItemsSource); collection.Filter = UnitysFilter; if (dataGridUnitys.Items.Count > 0) @@ -446,7 +469,7 @@ void FilterUnitys() void FilterBuildReport() { - _filterString = txtSearchBoxBuildReport.Text; + _filterString = searchBoxBuildReport.SearchText; ICollectionView collection = CollectionViewSource.GetDefaultView(gridBuildReport.ItemsSource); collection.Filter = BuildReportFilter; //if (gridBuildReport.Items.Count > 0) @@ -570,6 +593,7 @@ void LoadSettings() useUnofficialReleaseList.IsChecked = Settings.Default.useUnofficialReleaseList; chkDisableUnityHubLaunch.IsChecked = Settings.Default.disableUnityHubLaunch; chkFetchAdditionalInfo.IsChecked = Settings.Default.fetchAdditionalInfo; + chkFetchOnlineTemplates.IsChecked = Settings.Default.fetchOnlineTemplates; chkEnablePlatformSelection.IsChecked = Settings.Default.enablePlatformSelection; chkRunAutomatically.IsChecked = Settings.Default.runAutomatically; @@ -737,14 +761,16 @@ void LoadSettings() { string filename = ((ConfigurationErrorsException)ex.InnerException).Filename; - if (MessageBox.Show("This may be due to a Windows crash/BSOD.\n" + + var res = MessageBox.Show("This may be due to a Windows crash/BSOD.\n" + "Click 'Yes' to use automatic backup (if exists, otherwise settings are reset), then start application again.\n\n" + - "Click 'No' to exit now (and delete user.config manually)\n\nCorrupted file: " + filename, + "Click 'No' to reset config file (you'll need to setup settings again)\n\n" + + "Click 'Cancel' to exit now (and delete user.config manually)\n\nCorrupted file: " + filename, appName + " - Corrupt user settings", - MessageBoxButton.YesNo, - MessageBoxImage.Error) == MessageBoxResult.Yes) - { + MessageBoxButton.YesNoCancel, + MessageBoxImage.Error); + if (res == MessageBoxResult.Yes) + { // try to use backup string backupFilename = filename + ".bak"; if (File.Exists(backupFilename)) @@ -756,6 +782,15 @@ void LoadSettings() File.Delete(filename); } } + else if (res == MessageBoxResult.No) + { + File.Delete(filename); + } + else if (res == MessageBoxResult.Cancel) + { + Tools.ExploreFolder(Path.GetDirectoryName(filename)); + } + // need to restart, otherwise settings not loaded Process.GetCurrentProcess().Kill(); } @@ -880,11 +915,18 @@ void AddUnityInstallationRootFolder() var result = dialog.ShowDialog(); var newRoot = dialog.SelectedPath; + + if (lstRootFolders.Items.Contains(newRoot) == true) + { + SetStatus("Folder already exists in the list!", MessageType.Error); + return; + } + if (String.IsNullOrWhiteSpace(newRoot) == false && Directory.Exists(newRoot) == true) { - Properties.Settings.Default.rootFolders.Add(newRoot); + Settings.Default.rootFolders.Add(newRoot); lstRootFolders.Items.Refresh(); - Properties.Settings.Default.Save(); + Settings.Default.Save(); UpdateUnityInstallationsList(); RefreshRecentProjects(); } @@ -900,7 +942,7 @@ async Task CallGetUnityUpdates() if (updatesSource == null) return; dataGridUpdates.ItemsSource = updatesSource; // if search string is set, then filter it (after data is loaded) - if (string.IsNullOrEmpty(txtSearchBoxUpdates.Text) == false) + if (string.IsNullOrEmpty(searchBoxUpdates.SearchText) == false) { FilterUpdates(); } @@ -918,19 +960,19 @@ async void GoLookForUpdatesForThisUnity() { tabControl.SelectedIndex = 2; // need to clear old results first - txtSearchBoxUpdates.Text = ""; + searchBoxUpdates.SearchText = ""; // reset filter rdoAll.IsChecked = true; // NOTE for now, just set filter to current version, minus patch version "2021.1.7f1" > "2021.1" - txtSearchBoxUpdates.Text = unity.Version.Substring(0, unity.Version.LastIndexOf('.')); + searchBoxUpdates.SearchText = unity.Version.Substring(0, unity.Version.LastIndexOf('.')); } } public void RefreshRecentProjects() { // clear search - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = ""; // take currently selected project row lastSelectedProjectIndex = gridRecent.SelectedIndex; // rescan recent projects @@ -999,7 +1041,7 @@ private void BtnAddProjectFolder_Click(object sender, RoutedEventArgs e) //AddNewProjectToList(proj); Tools.AddProjectToHistory(proj.Path); // clear search, so can see added project - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = ""; RefreshRecentProjects(); // NOTE 0 works for sort-by-date only Tools.SetFocusToGrid(gridRecent, 0); @@ -1024,8 +1066,8 @@ void AddNewProjectToList(Project proj) gridRecent.SelectedIndex = 0; Tools.SetFocusToGrid(gridRecent); // force refresh - txtSearchBox.Text = proj.Title; - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = proj.Title; + searchBoxProjects.SearchText = ""; } private void BtnClose_Click(object sender, RoutedEventArgs e) @@ -1057,7 +1099,7 @@ private void OnGetUnityUpdatesClick(object sender, RoutedEventArgs e) // refresh installations, if already added some new ones UpdateUnityInstallationsList(); - txtSearchBoxUpdates.Text = ""; + searchBoxUpdates.SearchText = ""; // clear filters, since right now they are not used after updates are loaded rdoAll.IsChecked = true; CallGetUnityUpdates(); @@ -1084,19 +1126,19 @@ private void OnWindowKeyDown(object sender, KeyEventArgs e) break; case Key.Escape: // clear project search - if (txtSearchBox.Text == "") + if (searchBoxProjects.SearchText == "") { // its already clear } else // we have text in searchbox, clear it { - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = ""; } // try to keep selected row selected and in view Tools.SetFocusToGrid(gridRecent); break; case Key.F5: - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = ""; break; case Key.Up: case Key.Down: @@ -1130,10 +1172,10 @@ private void OnWindowKeyDown(object sender, KeyEventArgs e) if (Keyboard.Modifiers == ModifierKeys.Control) return; // activate searchbar if not active and we are in tab#1 - if (txtSearchBox.IsFocused == false) + if (searchBoxProjects.IsFocused == false) { - txtSearchBox.Focus(); - txtSearchBox.Select(txtSearchBox.Text.Length, 0); + searchBoxProjects.Focus(); + //searchBoxProjects.sele(searchBoxProjects.SearchText.Length, 0); } break; } @@ -1147,13 +1189,13 @@ private void OnWindowKeyDown(object sender, KeyEventArgs e) UpdateUnityInstallationsList(); break; case Key.Escape: // clear project search - txtSearchBoxUnity.Text = ""; + searchBoxUnitys.SearchText = ""; break; default: - if (txtSearchBoxUnity.IsFocused == false) + if (searchBoxUnitys.IsFocused == false) { - txtSearchBoxUnity.Focus(); - txtSearchBoxUnity.Select(txtSearchBoxUnity.Text.Length, 0); + searchBoxUnitys.Focus(); + //txtSearchBoxUnity.Select(txtSearchBoxUnity.Text.Length, 0); } break; } @@ -1167,7 +1209,7 @@ private void OnWindowKeyDown(object sender, KeyEventArgs e) CallGetUnityUpdates(); break; case Key.Escape: // clear project search - txtSearchBoxUpdates.Text = ""; + searchBoxUpdates.SearchText = ""; break; } break; @@ -1177,7 +1219,7 @@ private void OnWindowKeyDown(object sender, KeyEventArgs e) switch (e.Key) { case Key.Escape: // clear search - txtSearchBoxBuildReport.Text = ""; + searchBoxBuildReport.SearchText = ""; break; } break; @@ -1233,7 +1275,7 @@ private async void OnTabSelectionChanged(object sender, SelectionChangedEventArg if (updatesSource == null) return; dataGridUpdates.ItemsSource = updatesSource; // if search string is set, then filter it (after data is loaded) - if (string.IsNullOrEmpty(txtSearchBoxUpdates.Text) == false) + if (string.IsNullOrEmpty(searchBoxUpdates.SearchText) == false) { FilterUpdates(); } @@ -1243,18 +1285,18 @@ private async void OnTabSelectionChanged(object sender, SelectionChangedEventArg private void OnClearProjectSearchClick(object sender, RoutedEventArgs e) { - txtSearchBox.Text = ""; + searchBoxProjects.SearchText = ""; } private void OnClearUnitySearchClick(object sender, RoutedEventArgs e) { - txtSearchBoxUnity.Text = ""; + searchBoxUnitys.SearchText = ""; } private void OnClearUpdateSearchClick(object sender, RoutedEventArgs e) { // FIXME doesnt hide button, becaus button should have opposite of Text.IsEmpty, or custom style to hide when not empty - txtSearchBoxUpdates.Text = ""; + searchBoxUpdates.SearchText = ""; } private void Window_Closing(object sender, CancelEventArgs e) @@ -1350,6 +1392,14 @@ private void BtnUpgradeProject_Click(object sender, RoutedEventArgs e) if (proj == null) return; Tools.DisplayUpgradeDialog(proj: proj, owner: this, useInitScript: false); + + // update displayed version number now + var updatedVersion = Tools.GetProjectVersion(proj.Path); + if (string.IsNullOrEmpty(updatedVersion) == false) + { + proj.Version = updatedVersion; + gridRecent.Items.Refresh(); + } } private void GridRecent_Loaded(object sender, RoutedEventArgs e) @@ -1413,58 +1463,6 @@ private void BtnUpdateUnity_Click(object sender, RoutedEventArgs e) GoLookForUpdatesForThisUnity(); } - // if press up/down in search box, move to first item in results - private void TxtSearchBox_PreviewKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.Return: // open selected project - var proj = GetSelectedProject(); - var proc = Tools.LaunchProject(proj); - //ProcessHandler.Add(proj, proc); - break; - case Key.Tab: - case Key.Up: - //Tools.SetFocusToGrid(gridRecent); - var currentIndex = gridRecent.SelectedIndex - 1; - //Console.WriteLine(currentIndex); - if (currentIndex < 0) currentIndex = gridRecent.Items.Count - 1; - gridRecent.SelectedIndex = currentIndex; - e.Handled = true; - break; - case Key.Down: - // TODO move to 2nd row if first is already selected - //if (GetSelectedProjectIndex() == 0) - //{ - // Tools.SetFocusToGrid(gridRecent, 1); - //} - //else - //{ - //Tools.SetFocusToGrid(gridRecent); - // } - - // if in searchbox, then move selected index up or down - gridRecent.SelectedIndex = ++gridRecent.SelectedIndex % gridRecent.Items.Count; - e.Handled = true; // to stay in first row - break; - default: - break; - } - } - - private void TxtSearchBoxUnity_PreviewKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.Up: - case Key.Down: - Tools.SetFocusToGrid(dataGridUnitys); - break; - default: - break; - } - } - private void BtnAddInstallationFolder_Click(object sender, RoutedEventArgs e) { AddUnityInstallationRootFolder(); @@ -1497,12 +1495,15 @@ private void GridRecent_PreviewKeyDown(object sender, KeyEventArgs e) // if edit mode, dont override keys if (IsEditingCell(gridRecent) == true) return; // if in args column, dont jump to end of list, but end of this field - if (gridRecent.CurrentCell.Column.DisplayIndex == 4) + + var currentColumnCell = gridRecent.CurrentCell.Column.Header.ToString(); + if (currentColumnCell == "Arguments") { // start editing this cell gridRecent.BeginEdit(); return; } + gridRecent.SelectedIndex = gridRecent.Items.Count - 1; gridRecent.ScrollIntoView(gridRecent.SelectedItem); e.Handled = true; @@ -1567,20 +1568,6 @@ private void DataGridUpdates_PreviewKeyDown(object sender, KeyEventArgs e) } } - private void TxtSearchBoxUpdates_PreviewKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.Up: - case Key.Down: - Tools.SetFocusToGrid(dataGridUpdates); - e.Handled = true; - break; - default: - break; - } - } - private void BtnOpenEditorLogsFolder_Click(object sender, RoutedEventArgs e) { var logfolder = Tools.GetEditorLogsFolder(); @@ -1968,16 +1955,6 @@ private void GridRecent_CellEditEnding(object sender, DataGridCellEditEndingEven // TODO select the same row again } - private void TxtSearchBoxUpdates_TextChanged(object sender, TextChangedEventArgs e) - { - FilterUpdates(); - } - - private void TxtSearchBoxUnity_TextChanged(object sender, TextChangedEventArgs e) - { - FilterUnitys(); - } - private void GridRecent_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e) { // cancel if editing cell, because often try to double click to edit instead @@ -1987,8 +1964,11 @@ private void GridRecent_PreviewMouseDoubleClick(object sender, MouseButtonEventA if (e.OriginalSource.GetType() != typeof(TextBlock)) return; // cancel run if double clicked Arguments or Platform editable field - var currentColumnCell = gridRecent.CurrentCell.Column.DisplayIndex; - if (currentColumnCell == 4 || currentColumnCell == 6) return; + var currentColumnCell = gridRecent.CurrentCell.Column.Header.ToString(); + if (currentColumnCell == "Arguments" || currentColumnCell == "Platform") + { + return; + } var proj = GetSelectedProject(); var proc = Tools.LaunchProject(proj); @@ -2148,8 +2128,10 @@ void CreateNewEmptyProject(string targetFolder = null) } - var suggestedName = targetFolder != null ? Path.GetFileName(targetFolder) : Tools.GetSuggestedProjectName(newVersion, rootFolder); - NewProject modalWindow = new NewProject(newVersion, suggestedName, rootFolder, targetFolder != null); + string suggestedName = targetFolder != null ? Path.GetFileName(targetFolder) : Tools.GetSuggestedProjectName(newVersion, rootFolder); + bool fetchOnlineTemplates = chkFetchOnlineTemplates.IsChecked == true; + + NewProject modalWindow = new NewProject(newVersion, suggestedName, rootFolder, targetFolder != null, fetchOnlineTemplates); modalWindow.ShowInTaskbar = this == null; modalWindow.WindowStartupLocation = this == null ? WindowStartupLocation.CenterScreen : WindowStartupLocation.CenterOwner; modalWindow.Topmost = this == null; @@ -2964,8 +2946,8 @@ void ValidateFolderFromTextbox(TextBox textBox) if (Directory.Exists(textBox.Text) == true) { // NOTE this saves for shortcutbat setting, so cannot be used for another fields - Properties.Settings.Default.shortcutBatchFileFolder = textBox.Text; - Properties.Settings.Default.Save(); + Settings.Default.shortcutBatchFileFolder = textBox.Text; + Settings.Default.Save(); textBox.BorderBrush = System.Windows.Media.Brushes.Transparent; } else // invalid format @@ -3021,14 +3003,14 @@ private void ChkHumanFriendlyDateTime_Checked(object sender, RoutedEventArgs e) private void GridRecent_ColumnReordered(object sender, DataGridColumnEventArgs e) { // if amount has changed, need to reset array - if (Properties.Settings.Default.recentColumnsOrder.Length != gridRecent.Columns.Count) Properties.Settings.Default.recentColumnsOrder = new Int32[gridRecent.Columns.Count]; + if (Settings.Default.recentColumnsOrder.Length != gridRecent.Columns.Count) Properties.Settings.Default.recentColumnsOrder = new Int32[gridRecent.Columns.Count]; // get new display indexes for (int i = 0; i < gridRecent.Columns.Count; i++) { - Properties.Settings.Default.recentColumnsOrder[i] = gridRecent.Columns[i].DisplayIndex; + Settings.Default.recentColumnsOrder[i] = gridRecent.Columns[i].DisplayIndex; } - Properties.Settings.Default.Save(); + Settings.Default.Save(); } private void MenuItemExploreBuildItem_Click(object sender, RoutedEventArgs e) @@ -3080,25 +3062,7 @@ private void MenuItemUpdatesReleaseNotes_Click(object sender, RoutedEventArgs e) private void BtnClearBuildReportSearch_Click(object sender, RoutedEventArgs e) { - txtSearchBoxBuildReport.Text = ""; - } - - private void TxtSearchBoxBuildReport_PreviewKeyDown(object sender, KeyEventArgs e) - { - switch (e.Key) - { - case Key.Up: - case Key.Down: - Tools.SetFocusToGrid(gridBuildReport); - break; - default: - break; - } - } - - private void TxtSearchBoxBuildReport_TextChanged(object sender, TextChangedEventArgs e) - { - if (gridBuildReport.ItemsSource != null) FilterBuildReport(); + searchBoxBuildReport.SearchText = ""; } private void TxtLogCatArgs_TextChanged(object sender, TextChangedEventArgs e) @@ -4183,6 +4147,127 @@ private void gridRecent_SelectionChanged(object sender, SelectionChangedEventArg } + private void Image_MouseDown(object sender, MouseButtonEventArgs e) + { + var proj = GetSelectedProject(); + if (proj == null) return; + var thumbnailPath = Path.Combine(proj.Path, "ProjectSettings", "icon.png"); + Tools.LaunchExe(thumbnailPath); + } + + private void chkFetchOnlineTemplates_Checked(object sender, RoutedEventArgs e) + { + if (this.IsActive == false) return; + + Settings.Default.fetchOnlineTemplates = (bool)chkFetchOnlineTemplates.IsChecked; + Settings.Default.Save(); + } + + private void menuOpenReleasesApi_Click(object sender, RoutedEventArgs e) + { + var editor = GetSelectedUnity(); + if (editor == null || editor.Version == null) return; + Tools.OpenReleasesApiForVersion(editor.Version); + } + + private void SearchBoxProjects_SearchTextChanged(object sender, TextChangedEventArgs e) + { + FilterRecentProjects(); + } + + private void SearchBoxProjects_SearchKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Return: // open selected project + var proj = GetSelectedProject(); + var proc = Tools.LaunchProject(proj); + //ProcessHandler.Add(proj, proc); + break; + case Key.Tab: + case Key.Up: + //Tools.SetFocusToGrid(gridRecent); + var currentIndex = gridRecent.SelectedIndex - 1; + //Console.WriteLine(currentIndex); + if (currentIndex < 0) currentIndex = gridRecent.Items.Count - 1; + gridRecent.SelectedIndex = currentIndex; + e.Handled = true; + break; + case Key.Down: + // TODO move to 2nd row if first is already selected + //if (GetSelectedProjectIndex() == 0) + //{ + // Tools.SetFocusToGrid(gridRecent, 1); + //} + //else + //{ + //Tools.SetFocusToGrid(gridRecent); + // } + + // if in searchbox, then move selected index up or down + gridRecent.SelectedIndex = ++gridRecent.SelectedIndex % gridRecent.Items.Count; + e.Handled = true; // to stay in first row + break; + default: + break; + } + } + + private void searchBoxUnitys_SearchTextChanged(object sender, TextChangedEventArgs e) + { + FilterUnitys(); + } + + private void searchBoxUnitys_SearchKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Up: + case Key.Down: + Tools.SetFocusToGrid(dataGridUnitys); + break; + default: + break; + } + } + + private void searchBoxUpdates_SearchTextChanged(object sender, TextChangedEventArgs e) + { + FilterUpdates(); + } + + private void searchBoxUpdates_SearchKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Up: + case Key.Down: + Tools.SetFocusToGrid(dataGridUpdates); + e.Handled = true; + break; + default: + break; + } + } + + private void searchBoxBuildReport_SearchTextChanged(object sender, TextChangedEventArgs e) + { + if (gridBuildReport.ItemsSource != null) FilterBuildReport(); + } + + private void searchBoxBuildReport_SearchKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Up: + case Key.Down: + Tools.SetFocusToGrid(gridBuildReport); + break; + default: + break; + } + } + //private void menuProjectProperties_Click(object sender, RoutedEventArgs e) //{ // var proj = GetSelectedProject(); diff --git a/UnityLauncherPro/NewProject.xaml b/UnityLauncherPro/NewProject.xaml index c1986db..2940f33 100644 --- a/UnityLauncherPro/NewProject.xaml +++ b/UnityLauncherPro/NewProject.xaml @@ -4,10 +4,16 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:UnityLauncherPro" + xmlns:data="clr-namespace:UnityLauncherPro.Data" + d:DataContext="{d:DesignInstance Type=local:NewProject}" mc:Ignorable="d" - Title="Create New Project" Height="480" Width="500" Background="{DynamicResource ThemeDarkestBackground}" PreviewKeyDown="Window_PreviewKeyDown" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" ShowInTaskbar="True"> + Title="Create New Project" Height="520" Width="640" Background="{DynamicResource ThemeDarkestBackground}" PreviewKeyDown="Window_PreviewKeyDown" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" ShowInTaskbar="True"> + + + + + + + + + + + + + + diff --git a/UnityLauncherPro/NewProject.xaml.cs b/UnityLauncherPro/NewProject.xaml.cs index 3a11f14..e38cb04 100644 --- a/UnityLauncherPro/NewProject.xaml.cs +++ b/UnityLauncherPro/NewProject.xaml.cs @@ -1,10 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using UnityLauncherPro.Data; +using UnityLauncherPro.Properties; namespace UnityLauncherPro { @@ -21,16 +27,23 @@ public partial class NewProject : Window bool isInitializing = true; // to keep OnChangeEvent from firing too early int previousSelectedTemplateIndex = -1; int previousSelectedModuleIndex = -1; + bool loadOnlineTemplates = true; public static string targetFolder { get; private set; } = null; + private CancellationTokenSource _templateLoadCancellation; - public NewProject(string unityVersion, string suggestedName, string targetFolder, bool nameIsLocked = false) + public NewProject(string unityVersion, string suggestedName, string targetFolder, bool nameIsLocked = false, bool fetchOnlineTemplates = false) { isInitializing = true; InitializeComponent(); + loadOnlineTemplates = fetchOnlineTemplates; + btnFetchTemplates.Visibility = fetchOnlineTemplates ? Visibility.Collapsed : Visibility.Visible; + NewProject.targetFolder = targetFolder; + LoadSettings(); + // get version newVersion = unityVersion; newName = suggestedName; @@ -56,6 +69,9 @@ public NewProject(string unityVersion, string suggestedName, string targetFolder { gridAvailableVersions.SelectedIndex = i; gridAvailableVersions.ScrollIntoView(gridAvailableVersions.SelectedItem); + + string baseVersion = GetBaseVersion(newVersion); + if (fetchOnlineTemplates) _ = LoadOnlineTemplatesAsync(baseVersion); break; } } @@ -79,6 +95,11 @@ public NewProject(string unityVersion, string suggestedName, string targetFolder isInitializing = false; } // NewProject + private void LoadSettings() + { + chkForceDX11.IsChecked = Settings.Default.forceDX11; + } + void UpdateTemplatesDropDown(string unityPath) { // scan available templates, TODO could cache this at least per session? @@ -87,7 +108,6 @@ void UpdateTemplatesDropDown(string unityPath) lblTemplateTitleAndCount.Content = "Templates: (" + (cmbNewProjectTemplate.Items.Count - 1) + ")"; } - void UpdateModulesDropdown(string version) { // get modules and stick into combobox, NOTE we already have this info from GetProjects.Scan, so could access it @@ -135,7 +155,43 @@ private void BtnCreateNewProject_Click(object sender, RoutedEventArgs e) return; } - templateZipPath = ((KeyValuePair)cmbNewProjectTemplate.SelectedValue).Value; + // Check if online template is selected + if (listOnlineTemplates.SelectedItem is OnlineTemplateItem selectedOnlineTemplate) + { + // Use online template path + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string templatesPath = Path.Combine(appDataPath, "UnityHub", "Templates"); + + if (!string.IsNullOrEmpty(selectedOnlineTemplate.TarBallURL)) + { + string fileName = Path.GetFileName(new Uri(selectedOnlineTemplate.TarBallURL).LocalPath); + if (string.IsNullOrEmpty(fileName)) + { + string safeFileName = string.Join("_", selectedOnlineTemplate.Name.Split(Path.GetInvalidFileNameChars())); + fileName = $"{safeFileName}.tgz"; + } + + templateZipPath = Path.Combine(templatesPath, fileName); + + // Verify the file exists + if (!File.Exists(templateZipPath)) + { + Tools.SetStatus("Selected online template is not downloaded. Please download it first."); + return; + } + } + else + { + Tools.SetStatus("Invalid online template URL"); + return; + } + } + else + { + // Use built-in template from dropdown + templateZipPath = ((KeyValuePair)cmbNewProjectTemplate.SelectedValue).Value; + } + selectedPlatform = cmbNewProjectPlatform.SelectedValue.ToString(); UpdateSelectedVersion(); @@ -146,6 +202,7 @@ private void BtnCreateNewProject_Click(object sender, RoutedEventArgs e) DialogResult = true; } + private void BtnCancelNewProject_Click(object sender, RoutedEventArgs e) { DialogResult = false; @@ -292,7 +349,30 @@ private void GridAvailableVersions_SelectionChanged(object sender, SelectionChan // hide forceDX11 checkbox if version is below 6000 bool is6000 = k.Version.Contains("6000"); - chkForceDX11.Visibility = is6000 ? Visibility.Visible : Visibility.Collapsed; + lblOverride.Visibility = chkForceDX11.Visibility = is6000 ? Visibility.Visible : Visibility.Collapsed; + //chkForceDX11.IsChecked = chkForceDX11.Visibility == Visibility.Visible ? forceDX11 : false; + forceDX11 = Settings.Default.forceDX11 && is6000; + + listOnlineTemplates.ItemsSource = null; // clear previous items + if (loadOnlineTemplates) + { + string baseVersion = GetBaseVersion(k.Version); + // Cancel previous request + _templateLoadCancellation?.Cancel(); + _templateLoadCancellation = new CancellationTokenSource(); + _ = LoadOnlineTemplatesAsync(baseVersion, _templateLoadCancellation.Token); + } + } + + string GetBaseVersion(string version) + { + // e.g. 2020.3.15f1 -> 2020.3 + var parts = version.Split('.'); + if (parts.Length >= 2) + { + return parts[0] + "." + parts[1]; + } + return version; } private void GridAvailableVersions_Loaded(object sender, RoutedEventArgs e) @@ -330,12 +410,38 @@ private void gridAvailableVersions_PreviewMouseDoubleClick(object sender, MouseB private void chkForceDX11_Checked(object sender, RoutedEventArgs e) { - forceDX11 = chkForceDX11.IsChecked == true; + if (isInitializing) return; // Don't save during initialization + + Settings.Default.forceDX11 = forceDX11; + Settings.Default.Save(); } private void btnBrowseForProjectFolder_Click(object sender, RoutedEventArgs e) { - var folder = Tools.BrowseForOutputFolder("Select New Project folder"); + string defaultFolder = null; + if (txtNewProjectFolder.Text != null) + { + if (Directory.Exists(txtNewProjectFolder.Text) == true) + { + defaultFolder = txtNewProjectFolder.Text; + } + else + { + // find closest existing parent folder + var dir = new DirectoryInfo(txtNewProjectFolder.Text); + while (dir.Parent != null) + { + dir = dir.Parent; + if (Directory.Exists(dir.FullName) == true) + { + defaultFolder = dir.FullName; + break; + } + } + } + } + + var folder = Tools.BrowseForOutputFolder("Select New Project folder", defaultFolder); if (string.IsNullOrEmpty(folder) == false && Directory.Exists(folder) == true) { txtNewProjectFolder.Text = folder; @@ -350,13 +456,417 @@ private void txtNewProjectFolder_TextChanged(object sender, TextChangedEventArgs { txtNewProjectFolder.BorderBrush = Brushes.Red; // not visible if focused btnCreateNewProject.IsEnabled = false; + btnCreateMissingFolder.IsEnabled = true; } else { txtNewProjectFolder.BorderBrush = null; btnCreateNewProject.IsEnabled = true; targetFolder = txtNewProjectFolder.Text; + btnCreateMissingFolder.IsEnabled = false; + } + } + + private void btnCreateMissingFolder_Click(object sender, RoutedEventArgs e) + { + try + { + Directory.CreateDirectory(txtNewProjectFolder.Text); + txtNewProjectFolder.BorderBrush = null; + btnCreateNewProject.IsEnabled = true; + targetFolder = txtNewProjectFolder.Text; + } + catch (Exception ex) + { + Tools.SetStatus("Failed to create folder: " + ex.Message); + } + } + + private async Task LoadOnlineTemplatesAsync(string baseVersion, CancellationToken cancellationToken = default) + { + try + { + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.Add("Accept", "application/json"); + + var graphqlJson = "{\"query\":\"fragment TemplateEntity on Template { __typename name packageName description type buildPlatforms renderPipeline previewImage { url } versions { name tarball { url } } } query HUB__getTemplates($limit: Int! $skip: Int! $orderBy: TemplateOrder! $supportedUnityEditorVersions: [String!]!) { getTemplates(limit: $limit skip: $skip orderBy: $orderBy supportedUnityEditorVersions: $supportedUnityEditorVersions) { edges { node { ...TemplateEntity } } } }\",\"variables\":{\"limit\":40,\"skip\":0,\"orderBy\":\"WEIGHTED_DESC\",\"supportedUnityEditorVersions\":[\"" + baseVersion + "\"]}}"; + + var content = new StringContent(graphqlJson, Encoding.UTF8, "application/json"); + + // Check for cancellation before making request + if (cancellationToken.IsCancellationRequested) return; + + var response = await client.PostAsync("https://live-platform-api.prd.ld.unity3d.com/graphql", content, cancellationToken); + + // Check for cancellation after request + if (cancellationToken.IsCancellationRequested) return; + + if (response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync(); + + // Check for cancellation before parsing + if (cancellationToken.IsCancellationRequested) return; + + var templates = ParseTemplatesFromJson(responseString); + + // Update UI on dispatcher thread only if not cancelled + if (!cancellationToken.IsCancellationRequested) + { + Dispatcher.Invoke(() => + { + // Only set ItemsSource, don't touch Items + listOnlineTemplates.ItemsSource = templates; + }); + } + } + else + { + Console.WriteLine($"GraphQL request failed: {response.StatusCode}"); + if (!cancellationToken.IsCancellationRequested) + { + //LoadFallbackTemplates(); + } + } + } + } + catch (OperationCanceledException) + { + // Request was cancelled, this is expected + Console.WriteLine("Template loading cancelled"); + } + catch (Exception ex) + { + if (!cancellationToken.IsCancellationRequested) + { + Console.WriteLine($"Error loading online templates: {ex.Message}"); + //LoadFallbackTemplates(); + } + } + } + + private void LoadFallbackTemplates() + { + var templates = new List + { + new OnlineTemplateItem + { + Name = "3D Template", + Description = "A great starting point for 3D projects using the Universal Render Pipeline (URP).", + PreviewImageURL = "pack://application:,,,/Images/icon.png", + Type = "CORE", + RenderPipeline = "URP" + } + }; + + Dispatcher.Invoke(() => + { + // Only set ItemsSource, don't use Items.Clear() + listOnlineTemplates.ItemsSource = templates; + }); + } + + private List ParseTemplatesFromJson(string json) + { + var templates = new List(); + + try + { + // Get templates directory path + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string templatesPath = Path.Combine(appDataPath, "UnityHub", "Templates"); + + // Find the edges array + int edgesStart = json.IndexOf("\"edges\":"); + if (edgesStart == -1) return templates; + + // Find all node objects + int currentPos = edgesStart; + while (true) + { + int nodeStart = json.IndexOf("{\"__typename\":\"Template\"", currentPos); + if (nodeStart == -1) break; + + // Find the end of this node object (simplified - find matching brace) + int nodeEnd = FindMatchingBrace(json, nodeStart); + if (nodeEnd == -1) break; + + string nodeJson = json.Substring(nodeStart, nodeEnd - nodeStart + 1); + + // Parse individual fields + var tarballUrl = ExtractNestedJsonString(nodeJson, "\"tarball\"", "\"url\""); + var rawDescription = ExtractJsonString(nodeJson, "\"description\""); + var splitDescription = SplitTextToRows(rawDescription, 3); + + var template = new OnlineTemplateItem + { + Name = ExtractJsonString(nodeJson, "\"name\""), + Description = splitDescription, + Type = ExtractJsonString(nodeJson, "\"type\""), + RenderPipeline = ExtractJsonString(nodeJson, "\"renderPipeline\""), + PreviewImageURL = ExtractNestedJsonString(nodeJson, "\"previewImage\"", "\"url\"") ?? "pack://application:,,,/Images/icon.png", + TarBallURL = tarballUrl, + IsDownloaded = false + }; + + // Check if template file already exists + if (!string.IsNullOrEmpty(tarballUrl) && Directory.Exists(templatesPath)) + { + try + { + string fileName = Path.GetFileName(new Uri(tarballUrl).LocalPath); + if (!string.IsNullOrEmpty(fileName)) + { + string filePath = Path.Combine(templatesPath, fileName); + template.IsDownloaded = File.Exists(filePath); + } + } + catch + { + // If URL parsing fails, keep IsDownloaded as false + } + } + + templates.Add(template); + currentPos = nodeEnd + 1; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error parsing templates: {ex.Message}"); + } + + return templates; + } + + private string ExtractJsonString(string json, string key) + { + int keyIndex = json.IndexOf(key + ":"); + if (keyIndex == -1) return null; + + int valueStart = json.IndexOf("\"", keyIndex + key.Length + 1); + if (valueStart == -1) return null; + + int valueEnd = json.IndexOf("\"", valueStart + 1); + if (valueEnd == -1) return null; + + return json.Substring(valueStart + 1, valueEnd - valueStart - 1); + } + + private string ExtractNestedJsonString(string json, string parentKey, string childKey) + { + int parentIndex = json.IndexOf(parentKey + ":"); + if (parentIndex == -1) return null; + + // Find the object after parentKey + int objectStart = json.IndexOf("{", parentIndex); + if (objectStart == -1) return null; + + int objectEnd = FindMatchingBrace(json, objectStart); + if (objectEnd == -1) return null; + + string nestedJson = json.Substring(objectStart, objectEnd - objectStart + 1); + return ExtractJsonString(nestedJson, childKey); + } + + private int FindMatchingBrace(string json, int openBraceIndex) + { + int braceCount = 0; + bool inString = false; + bool escapeNext = false; + + for (int i = openBraceIndex; i < json.Length; i++) + { + char c = json[i]; + + if (escapeNext) + { + escapeNext = false; + continue; + } + + if (c == '\\') + { + escapeNext = true; + continue; + } + + if (c == '"') + { + inString = !inString; + continue; + } + + if (!inString) + { + if (c == '{') braceCount++; + else if (c == '}') + { + braceCount--; + if (braceCount == 0) return i; + } + } + } + + return -1; + } // FindMatchingBrace + + + + private void listOnlineTemplates_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + // Get the item that was clicked + var listBox = sender as ListBox; + if (listBox == null) return; + + // Find the ListBoxItem that was clicked + var clickedElement = e.OriginalSource as DependencyObject; + while (clickedElement != null && clickedElement != listBox) + { + if (clickedElement is ListBoxItem) + { + var clickedItem = clickedElement as ListBoxItem; + + // If the clicked item is already selected, deselect it + if (clickedItem.IsSelected) + { + listBox.SelectedItem = null; + e.Handled = true; + return; + } + break; + } + clickedElement = VisualTreeHelper.GetParent(clickedElement); + } + } + + + private void listOnlineTemplates_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (listOnlineTemplates.SelectedItem is OnlineTemplateItem selectedTemplate) + { + lblSelectedTemplate.Content = selectedTemplate.Name; + lblSelectedTemplate.BorderThickness = new Thickness(1); + + // Disable built-in template dropdown when online template is selected + cmbNewProjectTemplate.IsEnabled = false; + cmbNewProjectTemplate.SelectedIndex = 0; // Reset to default + + // disable create button if template not downloaded yet + btnCreateNewProject.IsEnabled = selectedTemplate.IsDownloaded; + btnCreateNewProject.Content = selectedTemplate.IsDownloaded ? "Create Project" : "Download Template First >"; + } + else + { + lblSelectedTemplate.Content = "None"; + lblSelectedTemplate.BorderThickness = new Thickness(0); + + // Re-enable built-in template dropdown when no online template is selected + cmbNewProjectTemplate.IsEnabled = true; + + // enable create button + btnCreateNewProject.IsEnabled = true; + btnCreateNewProject.Content = "Create Project"; + } + } + + private async void btnDownloadTemplate_Click(object sender, RoutedEventArgs e) + { + var button = sender as Button; + if (button?.Tag is OnlineTemplateItem template) + { + if (string.IsNullOrEmpty(template.TarBallURL)) + { + Tools.SetStatus("No download URL available for this template"); + return; + } + + try + { + // Disable button during download + button.IsEnabled = false; + button.Content = "⏳"; + + // Create templates directory in %APPDATA%\UnityHub\Templates + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string templatesPath = Path.Combine(appDataPath, "UnityHub", "Templates"); + + if (!Directory.Exists(templatesPath)) + { + Directory.CreateDirectory(templatesPath); + } + + // Extract original filename from URL + string fileName = Path.GetFileName(new Uri(template.TarBallURL).LocalPath); + if (string.IsNullOrEmpty(fileName)) + { + // Fallback to sanitized template name if URL doesn't have a filename + string safeFileName = string.Join("_", template.Name.Split(Path.GetInvalidFileNameChars())); + fileName = $"{safeFileName}.tgz"; + } + + string targetFilePath = Path.Combine(templatesPath, fileName); + + // Download the template (overwrite if exists) + using (var client = new HttpClient()) + { + var response = await client.GetAsync(template.TarBallURL); + response.EnsureSuccessStatusCode(); + + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + File.WriteAllBytes(targetFilePath, fileBytes); + + // Mark as downloaded + template.IsDownloaded = true; + + Tools.SetStatus($"Template downloaded: {template.Name}"); + + // Refresh no longer needed with INotifyPropertyChanged + // listOnlineTemplates.Items.Refresh(); + } + } + catch (Exception ex) + { + Tools.SetStatus($"Download failed: {ex.Message}"); + Console.WriteLine($"Error downloading template: {ex.Message}"); + } + finally + { + // Re-enable button + button.IsEnabled = true; + button.Content = "⬇"; + } + } + } // btnDownloadTemplate_Click + + private string SplitTextToRows(string description, int rows) + { + if (rows < 2) return description; + if (string.IsNullOrEmpty(description)) return description; + + int len = description.Length; + if (len <= rows) return description; // too short to split meaningfully + + int firstCut = (len / rows); + int secondCut = (len * 2) / rows; + + string part1 = description.Substring(0, firstCut).Trim(); + string part2 = description.Substring(firstCut, secondCut - firstCut).Trim(); + string part3 = description.Substring(secondCut).Trim(); + + return part1 + Environment.NewLine + part2 + Environment.NewLine + part3; + } + + private void btnFetchTemplates_Click(object sender, RoutedEventArgs e) + { + if (gridAvailableVersions.SelectedItem is UnityInstallation selectedInstallation) + { + string baseVersion = GetBaseVersion(selectedInstallation.Version); + _templateLoadCancellation?.Cancel(); + _templateLoadCancellation = new CancellationTokenSource(); + _ = LoadOnlineTemplatesAsync(baseVersion, _templateLoadCancellation.Token); } } - } -} + } // class NewProject +} // namespace UnityLauncherPro diff --git a/UnityLauncherPro/Properties/Settings.Designer.cs b/UnityLauncherPro/Properties/Settings.Designer.cs index 448598d..f71ec5c 100644 --- a/UnityLauncherPro/Properties/Settings.Designer.cs +++ b/UnityLauncherPro/Properties/Settings.Designer.cs @@ -637,5 +637,29 @@ public bool fetchAdditionalInfo { this["fetchAdditionalInfo"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool forceDX11 { + get { + return ((bool)(this["forceDX11"])); + } + set { + this["forceDX11"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool fetchOnlineTemplates { + get { + return ((bool)(this["fetchOnlineTemplates"])); + } + set { + this["fetchOnlineTemplates"] = value; + } + } } } diff --git a/UnityLauncherPro/Properties/Settings.settings b/UnityLauncherPro/Properties/Settings.settings index 6cea055..f47925b 100644 --- a/UnityLauncherPro/Properties/Settings.settings +++ b/UnityLauncherPro/Properties/Settings.settings @@ -160,5 +160,11 @@ False + + False + + + True + \ No newline at end of file diff --git a/UnityLauncherPro/Tools.cs b/UnityLauncherPro/Tools.cs index d245847..4de4fba 100644 --- a/UnityLauncherPro/Tools.cs +++ b/UnityLauncherPro/Tools.cs @@ -226,7 +226,7 @@ public static void AddProjectToHistory(string projectPath) } // 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) + public static Process LaunchProject(Project proj, DataGrid dataGridRef = null, bool useInitScript = false, bool upgrade = false, bool cloneFromTemplate = false) { if (proj == null) return null; @@ -274,6 +274,13 @@ public static Process LaunchProject(Project proj, DataGrid dataGridRef = null, b var unityExePath = GetUnityExePath(proj.Version); if (unityExePath == null) { + // if no editors installed, show message + if (MainWindow.unityInstallationsSource.Count == 0) + { + MessageBox.Show($"No Unity versions installed. Please run {MainWindow.appName} first to setup root folders.", MainWindow.appName, MessageBoxButton.OK, MessageBoxImage.Warning); + return null; + } + DisplayUpgradeDialog(proj, null, useInitScript); return null; } @@ -291,7 +298,7 @@ public static Process LaunchProject(Project proj, DataGrid dataGridRef = null, b var cmd = "\"" + unityExePath + "\""; newProcess.StartInfo.FileName = cmd; - var unitycommandlineparameters = " -projectPath " + "\"" + proj.Path + "\""; + string unitycommandlineparameters = (cloneFromTemplate ? " -createproject " : " -projectPath ") + "\"" + proj.Path + "\""; string customArguments = proj.Arguments; if (string.IsNullOrEmpty(customArguments) == false) @@ -1285,7 +1292,7 @@ public static void DisplayUpgradeDialog(Project proj, MainWindow owner, bool use // 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 + // inject new version for this item proj.Version = upgradeToVersion; SaveProjectVersion(proj); var proc = LaunchProject(proj, dataGridRef: null, useInitScript: false, upgrade: true); @@ -1665,6 +1672,9 @@ public static void SetFocusToGrid(DataGrid targetGrid, int index = -1) DataGridRow row = (DataGridRow)targetGrid.ItemContainerGenerator.ContainerFromIndex(index); if (row == null) { + // clamp to max items + if (index >= targetGrid.Items.Count) index = targetGrid.Items.Count - 1; + targetGrid.ScrollIntoView(targetGrid.Items[index]); // Defer the focus once row is generated targetGrid.Dispatcher.InvokeAsync(() => @@ -1750,6 +1760,7 @@ public static Project FastCreateProject(string version, string baseFolder, strin // create folder CreateEmptyProjectFolder(newPath, version); + bool cloneFromTemplate = false; // unzip or copy template if (templateZipPath != null) { @@ -1757,9 +1768,11 @@ public static Project FastCreateProject(string version, string baseFolder, strin if (File.Exists(templateZipPath)) { + cloneFromTemplate = true; try { - TarLib.Tar.ExtractTarGz(templateZipPath, newPath); + // NOTE no need to extract, unity can handle it with -cloneFromTemplate + //TarLib.Tar.ExtractTarGz(templateZipPath, newPath); } catch (Exception ex) { @@ -1768,6 +1781,7 @@ public static Project FastCreateProject(string version, string baseFolder, strin } else if (Directory.Exists(templateZipPath)) { + // this is for folder templates, copy files try { CopyDirectory(templateZipPath, newPath); @@ -1803,7 +1817,11 @@ public static Project FastCreateProject(string version, string baseFolder, strin proj.Modified = DateTime.Now; proj.folderExists = true; // have to set this value, so item is green on list proj.Arguments = version.Contains("6000") ? (forceDX11 ? "-force-d3d11" : null) : null; // this gets erased later, since its not saved? would be nice to not add it at all though - var proc = LaunchProject(proj, null, useInitScript); + if (cloneFromTemplate == true) + { + proj.Arguments += " -cloneFromTemplate \"" + templateZipPath + "\""; + } + var proc = LaunchProject(proj, null, useInitScript, false, cloneFromTemplate); ProcessHandler.Add(proj, proc); return proj; @@ -3131,7 +3149,27 @@ public static string ParseHashCodeFromURL(string url) return url.Substring(hashStart, hashEnd - hashStart); } + internal static void OpenReleasesApiForVersion(string version) + { + string url = $"https://services.api.unity.com/unity/editor/release/v1/releases?limit=1&version={version}"; + LaunchBrowser(url); + } + private static void LaunchBrowser(string url) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch (Exception ex) + { + MessageBox.Show("Failed to open URL: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + } } // class } // namespace diff --git a/UnityLauncherPro/UnityLauncherPro.csproj b/UnityLauncherPro/UnityLauncherPro.csproj index 5fe024b..9f3831c 100644 --- a/UnityLauncherPro/UnityLauncherPro.csproj +++ b/UnityLauncherPro/UnityLauncherPro.csproj @@ -83,14 +83,20 @@ MSBuild:Compile Designer + + SearchBoxControl.xaml + + + + @@ -119,6 +125,9 @@ UpgradeWindow.xaml + + MSBuild:Compile + Designer diff --git a/UnityLauncherPro/UpgradeWindow.xaml.cs b/UnityLauncherPro/UpgradeWindow.xaml.cs index 9c1c48f..22f9477 100644 --- a/UnityLauncherPro/UpgradeWindow.xaml.cs +++ b/UnityLauncherPro/UpgradeWindow.xaml.cs @@ -26,11 +26,6 @@ public UpgradeWindow(string currentVersion, string projectPath, string commandLi gridAvailableVersions.SelectedItem = null; - // we have current version info in project - // enable release and dl buttons - btnOpenReleasePage.IsEnabled = true; - btnDownload.IsEnabled = true; - // if dont have exact version, show red outline if (currentVersion == null || MainWindow.unityInstalledVersions.ContainsKey(currentVersion) == false) { @@ -40,6 +35,10 @@ public UpgradeWindow(string currentVersion, string projectPath, string commandLi if (currentVersion != null) { + // we know the version, enable buttons + btnOpenReleasePage.IsEnabled = true; + btnDownload.IsEnabled = true; + // remove china c1 from version if (currentVersion.Contains("c")) currentVersion = currentVersion.Replace("c1", ""); // find nearest version @@ -150,6 +149,13 @@ private void GridAvailableVersions_PreviewMouseDoubleClick(object sender, MouseB void Upgrade() { var k = (UnityInstallation)gridAvailableVersions.SelectedItem; + + if (k == null) + { + DialogResult = false; + return; + } + upgradeVersion = k.Version; DialogResult = true; }