├── .github
└── workflows
│ ├── codeql.yml
│ └── main.yml
├── .gitignore
├── ArtifactBuild.cmd
├── Build.cmd
├── BuildInstaller.bat
├── CONTRIBUTING.md
├── LICENSE
├── Paths.cmd.template
├── README.md
├── UnityLauncherPro.sln
├── UnityLauncherPro
├── App.config
├── App.xaml
├── App.xaml.cs
├── Converters
│ ├── LastModifiedConverter.cs
│ └── ReleaseDateConverter.cs
├── Data
│ ├── BuildReport.cs
│ ├── BuildReportItem.cs
│ ├── DownloadProgress.cs
│ ├── MessageType.cs
│ ├── Platform.cs
│ ├── Project.cs
│ ├── Tabs.cs
│ ├── ThemeColor.cs
│ ├── UnityInstallation.cs
│ ├── UnityVersion.cs
│ ├── UnityVersionResponse.cs
│ └── UnityVersionStream.cs
├── DownloadProgressWindow.xaml
├── DownloadProgressWindow.xaml.cs
├── GetProjects.cs
├── GetUnityInstallations.cs
├── GetUnityUpdates.cs
├── Helpers
│ ├── ObservableDictionary.cs
│ └── ProcessHandler.cs
├── Images
│ ├── icon.ico
│ └── icon.png
├── Libraries
│ └── ExtractTarGz.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── NewProject.xaml
├── NewProject.xaml.cs
├── ProjectProperties.xaml
├── ProjectProperties.xaml.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── Resources
│ ├── Colors.xaml
│ └── UnityVersionCache.json
├── ThemeEditor.xaml
├── ThemeEditor.xaml.cs
├── Tools.cs
├── UnityLauncherPro.csproj
├── UpgradeWindow.xaml
├── UpgradeWindow.xaml.cs
├── Version.cs
└── app.manifest
├── UnityLauncherProInstaller
└── UnityLauncherProInstaller.vdproj
└── appveyor.yml
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '19 2 * * 5'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: windows-2019
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'csharp' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 |
4 | on:
5 | # Triggers the workflow on push or pull request events but only for the master branch
6 | push:
7 | branches: [ master ]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | jobs:
13 | # This workflow contains a single job called "build"
14 | build:
15 | # The type of runner that the job will run on
16 | runs-on: windows-2022
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # Step to check if the commit message contains #GITBUILD
21 | - name: Check Commit Message
22 | shell: powershell
23 | run: |
24 | # Get the commit message
25 | $strVal = '${{ github.event.commits[0].message }}'
26 | # Convert commit message to a single line if multiline
27 | $singleLineStrVal = $strVal -replace "`r`n", " " -replace "`n", " "
28 | if ($singleLineStrVal -match '#GITBUILD') {
29 | Write-Host 'Build commit detected. Proceeding with build...'
30 | echo "build_trigger=true" >> $env:GITHUB_ENV
31 | } else {
32 | Write-Host 'No build commit. Skipping build steps...'
33 | echo "build_trigger=false" >> $env:GITHUB_ENV
34 | }
35 |
36 | # Step to ensure the repository is checked out
37 | - uses: actions/checkout@v2
38 |
39 | # Inform if build steps are skipped
40 | - name: Inform Skipped Build Steps
41 | if: env.build_trigger != 'true'
42 | shell: powershell
43 | run: |
44 | Write-Host "Skipping build steps because the commit message does not contain #GITBUILD."
45 |
46 | # Install 7Zip PowerShell module
47 | - name: Install 7Zip PowerShell Module
48 | if: env.build_trigger == 'true'
49 | shell: powershell
50 | run: Install-Module 7Zip4PowerShell -Force -Verbose
51 |
52 | # Restore NuGet packages
53 | - name: Restore NuGet packages
54 | if: env.build_trigger == 'true'
55 | run: nuget restore UnityLauncherPro.sln
56 |
57 | # Build the binary
58 | - name: Build Binary
59 | if: env.build_trigger == 'true'
60 | shell: cmd
61 | run: call .\Build.cmd
62 |
63 | # Build the artifact
64 | - name: Build Artifact
65 | if: env.build_trigger == 'true'
66 | shell: cmd
67 | run: call .\ArtifactBuild.cmd
68 |
69 | # Check that output exists
70 | - name: Validate UnityLauncherPro.exe exists
71 | if: env.build_trigger == 'true'
72 | shell: cmd
73 | run: |
74 | if not exist "UnityLauncherPro\bin\Release\UnityLauncherPro.exe" (
75 | echo ERROR: UnityLauncherPro.exe not found.
76 | exit /b 1
77 | )
78 | echo Found UnityLauncherPro.exe
79 |
80 | # 1) Compute a wrapped major.minor.build from the run number
81 | - name: Compute installer version
82 | if: env.build_trigger == 'true'
83 | shell: bash
84 | run: |
85 | TOTAL=${{ github.run_number }}
86 | BUILD=$(( TOTAL % 65536 ))
87 | MINOR_TOTAL=$(( TOTAL / 65536 ))
88 | MINOR=$(( MINOR_TOTAL % 256 ))
89 | MAJOR=$(( MINOR_TOTAL / 256 ))
90 | echo "INSTALLER_MAJOR=$MAJOR" >> $GITHUB_ENV
91 | echo "INSTALLER_MINOR=$MINOR" >> $GITHUB_ENV
92 | echo "INSTALLER_BUILD=$BUILD" >> $GITHUB_ENV
93 | echo "INSTALLER_VER=$MAJOR.$MINOR.$BUILD" >> $GITHUB_ENV
94 | echo "Computed installer version → $MAJOR.$MINOR.$BUILD (run #${{ github.run_number }})"
95 |
96 | # 2) Patch your .vdproj in place (inject ProductVersion and a new GUID)
97 | - name: Patch .vdproj ProductVersion & ProductCode
98 | if: env.build_trigger == 'true'
99 | shell: pwsh
100 | run: |
101 | $proj = 'UnityLauncherProInstaller\UnityLauncherProInstaller.vdproj'
102 | $ver = "${{ env.INSTALLER_VER }}" # e.g. 0.0.118
103 | $guid = [Guid]::NewGuid().ToString("B").ToUpper() # e.g. {E821A3F5-1F84-4A4B-BE9D-115D93E9E6F0}
104 |
105 | # Read & rewrite line-by-line
106 | $fixed = Get-Content $proj | ForEach-Object {
107 | if ($_ -match '^(\s*)"ProductVersion"') {
108 | # preserve indentation, then inject exactly one pair of braces
109 | "$($Matches[1])""ProductVersion"" = ""8:$ver"""
110 | }
111 | elseif ($_ -match '^(\s*)"ProductCode"') {
112 | "$($Matches[1])""ProductCode"" = ""8:$guid"""
113 | }
114 | else {
115 | $_
116 | }
117 | }
118 |
119 | # Overwrite the project
120 | Set-Content -Path $proj -Value $fixed
121 |
122 | Write-Host "→ ProductVersion patched to 8:$ver"
123 | Write-Host "→ ProductCode patched to 8:$guid"
124 |
125 |
126 | # 3) **DEBUG**: print out the patched .vdproj so you can inspect it
127 | - name: Show patched .vdproj
128 | if: env.build_trigger == 'true'
129 | shell: pwsh
130 | run: |
131 | $proj = 'UnityLauncherProInstaller\UnityLauncherProInstaller.vdproj'
132 | Write-Host "=== BEGIN .vdproj CONTENT ==="
133 | Get-Content $proj
134 | Write-Host "=== END .vdproj CONTENT ==="
135 |
136 | # locate VS 2022
137 | - name: Locate Visual Studio 2022
138 | id: vswhere
139 | shell: pwsh
140 | run: |
141 | $installPath = vswhere -latest -products * -requires Microsoft.Component.MSBuild `
142 | -property installationPath
143 | if (-not $installPath) { throw 'VS 2022 not found' }
144 | Write-Host "##[set-output name=installPath]$installPath"
145 |
146 | # download vs installer builder (v2.0.1)
147 | - name: Download Installer-Projects VSIX
148 | shell: pwsh
149 | run: |
150 | $vsixUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioClient/vsextensions/MicrosoftVisualStudio2022InstallerProjects/2.0.1/vspackage"
151 | Invoke-WebRequest $vsixUrl -OutFile installerprojects.vsix
152 |
153 | # install vs installer builder
154 | - name: Install Installer-Projects extension
155 | shell: pwsh
156 | run: |
157 | $vsixInstaller = Join-Path "${{ steps.vswhere.outputs.installPath }}" 'Common7\IDE\VSIXInstaller.exe'
158 | Write-Host "Running: $vsixInstaller installerprojects.vsix /quiet"
159 | & $vsixInstaller installerprojects.vsix /quiet
160 |
161 | # Build MSI installer project using Visual Studio 2022 workaround
162 | - name: Build Installer MSI
163 | if: env.build_trigger == 'true'
164 | shell: cmd
165 | run: |
166 | echo === Running BuildInstaller.bat ===
167 | call .\BuildInstaller.bat || echo WARNING: BuildInstaller.bat exited with %ERRORLEVEL%, continuing...
168 | echo === Verifying MSI ===
169 | if exist "UnityLauncherProInstaller\Release\UnityLauncherPro-Installer.msi" (
170 | echo Success: MSI found at UnityLauncherProInstaller\Release\UnityLauncherPro-Installer.msi
171 | exit /b 0
172 | ) else (
173 | echo ERROR: MSI not found in UnityLauncherProInstaller\Release
174 | exit /b 1
175 | )
176 |
177 | # Get the current date and time
178 | - name: Get current date and time
179 | id: datetime
180 | if: env.build_trigger == 'true' # Only run if build was triggered
181 | run: |
182 | # Save the current date and time to an environment variable
183 | echo "current_datetime=$(date +'%d/%m/%Y %H:%M')" >> $GITHUB_ENV
184 |
185 | # Step to get previous tag and commits
186 | - name: Get commits since last release
187 | id: get_commits
188 | if: env.build_trigger == 'true' # Only run if build was triggered
189 | shell: bash
190 | run: |
191 | # Get the most recent tag
192 | PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none")
193 | if [ "$PREV_TAG" = "none" ]; then
194 | echo "No previous tag found, listing all commits"
195 | COMMITS=$(git log --pretty=format:"* %s" --no-merges)
196 | else
197 | echo "Previous tag: $PREV_TAG"
198 | # List commits since last tag
199 | COMMITS=$(git log $PREV_TAG..HEAD --pretty=format:"* %s" --no-merges)
200 | fi
201 | # Save commits to the environment
202 | echo "commits=$COMMITS" >> $GITHUB_ENV
203 |
204 | # Create a release
205 | - name: Create Release
206 | id: create_release
207 | if: env.build_trigger == 'true' # Execute only if build was triggered
208 | uses: actions/create-release@latest
209 | env:
210 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
211 | with:
212 | tag_name: ${{github.run_number}}
213 | release_name: ${{ env.current_datetime }} (${{ github.run_number }})
214 | body: |
215 | Automated Release by GitHub Action CI
216 |
217 | ### Commits in this release:
218 | ${{ env.commits }}
219 | draft: false
220 | prerelease: false
221 |
222 | # Upload the release asset
223 | - name: Upload Release Asset
224 | id: upload-release-asset
225 | if: env.build_trigger == 'true' # Execute only if build was triggered
226 | uses: actions/upload-release-asset@v1
227 | env:
228 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
229 | with:
230 | upload_url: ${{ steps.create_release.outputs.upload_url }}
231 | asset_path: ./UnityLauncherPro.zip
232 | asset_name: UnityLauncherPro.zip
233 | asset_content_type: application/zip
234 |
235 | # Upload MSI installer to release
236 | - name: Upload MSI Installer
237 | if: env.build_trigger == 'true'
238 | uses: actions/upload-release-asset@v1
239 | env:
240 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
241 | with:
242 | upload_url: ${{ steps.create_release.outputs.upload_url }}
243 | asset_path: ./UnityLauncherProInstaller/Release/UnityLauncherPro-Installer.msi
244 | asset_name: UnityLauncherPro-Installer.msi
245 | asset_content_type: application/x-msi
246 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015/2017 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # Visual Studio 2017 auto generated files
33 | Generated\ Files/
34 |
35 | # MSTest test Results
36 | [Tt]est[Rr]esult*/
37 | [Bb]uild[Ll]og.*
38 |
39 | # NUNIT
40 | *.VisualState.xml
41 | TestResult.xml
42 |
43 | # Build Results of an ATL Project
44 | [Dd]ebugPS/
45 | [Rr]eleasePS/
46 | dlldata.c
47 |
48 | # Benchmark Results
49 | BenchmarkDotNet.Artifacts/
50 |
51 | # .NET Core
52 | project.lock.json
53 | project.fragment.lock.json
54 | artifacts/
55 | **/Properties/launchSettings.json
56 |
57 | # StyleCop
58 | StyleCopReport.xml
59 |
60 | # Files built by Visual Studio
61 | *_i.c
62 | *_p.c
63 | *_i.h
64 | *.ilk
65 | *.meta
66 | *.obj
67 | *.iobj
68 | *.pch
69 | *.pdb
70 | *.ipdb
71 | *.pgc
72 | *.pgd
73 | *.rsp
74 | *.sbr
75 | *.tlb
76 | *.tli
77 | *.tlh
78 | *.tmp
79 | *.tmp_proj
80 | *.log
81 | *.vspscc
82 | *.vssscc
83 | .builds
84 | *.pidb
85 | *.svclog
86 | *.scc
87 |
88 | # Chutzpah Test files
89 | _Chutzpah*
90 |
91 | # Visual C++ cache files
92 | ipch/
93 | *.aps
94 | *.ncb
95 | *.opendb
96 | *.opensdf
97 | *.sdf
98 | *.cachefile
99 | *.VC.db
100 | *.VC.VC.opendb
101 |
102 | # Visual Studio profiler
103 | *.psess
104 | *.vsp
105 | *.vspx
106 | *.sap
107 |
108 | # Visual Studio Trace Files
109 | *.e2e
110 |
111 | # TFS 2012 Local Workspace
112 | $tf/
113 |
114 | # Guidance Automation Toolkit
115 | *.gpState
116 |
117 | # ReSharper is a .NET coding add-in
118 | _ReSharper*/
119 | *.[Rr]e[Ss]harper
120 | *.DotSettings.user
121 |
122 | # JustCode is a .NET coding add-in
123 | .JustCode
124 |
125 | # TeamCity is a build add-in
126 | _TeamCity*
127 |
128 | # DotCover is a Code Coverage Tool
129 | *.dotCover
130 |
131 | # AxoCover is a Code Coverage Tool
132 | .axoCover/*
133 | !.axoCover/settings.json
134 |
135 | # Visual Studio code coverage results
136 | *.coverage
137 | *.coveragexml
138 |
139 | # NCrunch
140 | _NCrunch_*
141 | .*crunch*.local.xml
142 | nCrunchTemp_*
143 |
144 | # MightyMoose
145 | *.mm.*
146 | AutoTest.Net/
147 |
148 | # Web workbench (sass)
149 | .sass-cache/
150 |
151 | # Installshield output folder
152 | [Ee]xpress/
153 |
154 | # DocProject is a documentation generator add-in
155 | DocProject/buildhelp/
156 | DocProject/Help/*.HxT
157 | DocProject/Help/*.HxC
158 | DocProject/Help/*.hhc
159 | DocProject/Help/*.hhk
160 | DocProject/Help/*.hhp
161 | DocProject/Help/Html2
162 | DocProject/Help/html
163 |
164 | # Click-Once directory
165 | publish/
166 |
167 | # Publish Web Output
168 | *.[Pp]ublish.xml
169 | *.azurePubxml
170 | # Note: Comment the next line if you want to checkin your web deploy settings,
171 | # but database connection strings (with potential passwords) will be unencrypted
172 | *.pubxml
173 | *.publishproj
174 |
175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
176 | # checkin your Azure Web App publish settings, but sensitive information contained
177 | # in these scripts will be unencrypted
178 | PublishScripts/
179 |
180 | # NuGet Packages
181 | *.nupkg
182 | # The packages folder can be ignored because of Package Restore
183 | **/[Pp]ackages/*
184 | # except build/, which is used as an MSBuild target.
185 | !**/[Pp]ackages/build/
186 | # Uncomment if necessary however generally it will be regenerated when needed
187 | #!**/[Pp]ackages/repositories.config
188 | # NuGet v3's project.json files produces more ignorable files
189 | *.nuget.props
190 | *.nuget.targets
191 |
192 | # Microsoft Azure Build Output
193 | csx/
194 | *.build.csdef
195 |
196 | # Microsoft Azure Emulator
197 | ecf/
198 | rcf/
199 |
200 | # Windows Store app package directories and files
201 | AppPackages/
202 | BundleArtifacts/
203 | Package.StoreAssociation.xml
204 | _pkginfo.txt
205 | *.appx
206 |
207 | # Visual Studio cache files
208 | # files ending in .cache can be ignored
209 | *.[Cc]ache
210 | # but keep track of directories ending in .cache
211 | !*.[Cc]ache/
212 |
213 | # Others
214 | ClientBin/
215 | ~$*
216 | *~
217 | *.dbmdl
218 | *.dbproj.schemaview
219 | *.jfm
220 | *.pfx
221 | *.publishsettings
222 | orleans.codegen.cs
223 |
224 | # Including strong name files can present a security risk
225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
226 | #*.snk
227 |
228 | # Since there are multiple workflows, uncomment next line to ignore bower_components
229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
230 | #bower_components/
231 |
232 | # RIA/Silverlight projects
233 | Generated_Code/
234 |
235 | # Backup & report files from converting an old project file
236 | # to a newer Visual Studio version. Backup files are not needed,
237 | # because we have git ;-)
238 | _UpgradeReport_Files/
239 | Backup*/
240 | UpgradeLog*.XML
241 | UpgradeLog*.htm
242 | ServiceFabricBackup/
243 | *.rptproj.bak
244 |
245 | # SQL Server files
246 | *.mdf
247 | *.ldf
248 | *.ndf
249 |
250 | # Business Intelligence projects
251 | *.rdl.data
252 | *.bim.layout
253 | *.bim_*.settings
254 | *.rptproj.rsuser
255 |
256 | # Microsoft Fakes
257 | FakesAssemblies/
258 |
259 | # GhostDoc plugin setting file
260 | *.GhostDoc.xml
261 |
262 | # Node.js Tools for Visual Studio
263 | .ntvs_analysis.dat
264 | node_modules/
265 |
266 | # Visual Studio 6 build log
267 | *.plg
268 |
269 | # Visual Studio 6 workspace options file
270 | *.opt
271 |
272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
273 | *.vbw
274 |
275 | # Visual Studio LightSwitch build output
276 | **/*.HTMLClient/GeneratedArtifacts
277 | **/*.DesktopClient/GeneratedArtifacts
278 | **/*.DesktopClient/ModelManifest.xml
279 | **/*.Server/GeneratedArtifacts
280 | **/*.Server/ModelManifest.xml
281 | _Pvt_Extensions
282 |
283 | # Paket dependency manager
284 | .paket/paket.exe
285 | paket-files/
286 |
287 | # FAKE - F# Make
288 | .fake/
289 |
290 | # JetBrains Rider
291 | .idea/
292 | *.sln.iml
293 |
294 | # CodeRush
295 | .cr/
296 |
297 | # Python Tools for Visual Studio (PTVS)
298 | __pycache__/
299 | *.pyc
300 |
301 | # Cake - Uncomment if you are using it
302 | # tools/**
303 | # !tools/packages.config
304 |
305 | # Tabs Studio
306 | *.tss
307 |
308 | # Telerik's JustMock configuration file
309 | *.jmconfig
310 |
311 | # BizTalk build output
312 | *.btp.cs
313 | *.btm.cs
314 | *.odx.cs
315 | *.xsd.cs
316 |
317 | # OpenCover UI analysis results
318 | OpenCover/
319 |
320 | # Azure Stream Analytics local run output
321 | ASALocalRun/
322 |
323 | # MSBuild Binary and Structured Log
324 | *.binlog
325 |
326 | # NVidia Nsight GPU debugger configuration file
327 | *.nvuser
328 |
329 | # MFractors (Xamarin productivity tool) working folder
330 | .mfractor/
331 | /Paths.cmd
332 |
--------------------------------------------------------------------------------
/ArtifactBuild.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 | pushd "%~dp0"
3 | powershell Compress-7Zip "UnityLauncherPro\bin\Release\UnityLauncherPro.exe" -ArchiveFileName "UnityLauncherPro.zip" -Format Zip
4 | :exit
5 | popd
6 | @echo on
7 |
--------------------------------------------------------------------------------
/Build.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Default VS paths to check if no Paths.cmd file exists
4 | set VISUAL_STUDIO_PATH_0="%programfiles%\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\msbuild.exe"
5 | set VISUAL_STUDIO_PATH_1="%programfiles%\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\msbuild.exe"
6 | set VISUAL_STUDIO_PATH_2="%programfiles(x86)%\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe"
7 | set VISUAL_STUDIO_PATH_3="%programfiles(x86)%\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe"
8 |
9 | pushd "%~dp0"
10 | if exist Debug rd /s /q Debug
11 | if exist Release rd /s /q Release
12 | if exist x64 rd /s /q x64
13 |
14 | if exist "Paths.cmd" (
15 | REM Prefer Paths.cmd as Visual Studio path source if it exists.
16 | call Paths.cmd
17 | goto build
18 | ) else (
19 | REM Otherwise try to auto-detect the Visual Studio path.
20 | if exist %VISUAL_STUDIO_PATH_0% (
21 | set VISUAL_STUDIO_PATH=%VISUAL_STUDIO_PATH_0%
22 | goto build
23 | )
24 |
25 | if exist %VISUAL_STUDIO_PATH_1% (
26 | set VISUAL_STUDIO_PATH=%VISUAL_STUDIO_PATH_1%
27 | goto build
28 | )
29 |
30 | if exist %VISUAL_STUDIO_PATH_2% (
31 | set VISUAL_STUDIO_PATH=%VISUAL_STUDIO_PATH_2%
32 | goto build
33 | )
34 |
35 | if exist %VISUAL_STUDIO_PATH_3% (
36 | set VISUAL_STUDIO_PATH=%VISUAL_STUDIO_PATH_3%
37 | goto build
38 | )
39 |
40 | REM No default path found. Let the user know what to do.
41 | echo No Visual Studio installation found. Please configure it manually.
42 | echo 1. Copy 'Paths.cmd.template'.
43 | echo 2. Rename it to 'Paths.cmd'.
44 | echo 3. Enter your Visual Studio path in there.
45 | echo 4. Restart the build.
46 | REM Allow disabling pause to support non-interacting build chains.
47 | if NOT "%~1"=="-no-pause" pause
48 | goto end
49 | )
50 |
51 | :build
52 | REM Log the used Vistual Studio version.
53 | @echo on
54 | %VISUAL_STUDIO_PATH% /p:Configuration=Release
55 | @echo off
56 |
57 | :end
58 | popd
59 | @echo on
60 |
--------------------------------------------------------------------------------
/BuildInstaller.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 | SETLOCAL ENABLEEXTENSIONS ENABLEDELAYEDEXPANSION
3 |
4 | ECHO:
5 | ECHO === Starting Installer Build Workaround ===
6 |
7 | REM Store current directory
8 | SET "current_path=%CD%"
9 |
10 | REM Try all known editions of Visual Studio 2022
11 | SET "vs_base_path=%ProgramFiles%\Microsoft Visual Studio\2022"
12 | FOR %%E IN (Community Professional Enterprise) DO (
13 | IF EXIST "%vs_base_path%\%%E\Common7\IDE\CommonExtensions\Microsoft\VSI\DisableOutOfProcBuild\DisableOutOfProcBuild.exe" (
14 | SET "buildfix_path=%vs_base_path%\%%E\Common7\IDE\CommonExtensions\Microsoft\VSI\DisableOutOfProcBuild"
15 | SET "devenv_path=%vs_base_path%\%%E\Common7\IDE\devenv.exe"
16 | SET "vs_edition=%%E"
17 | GOTO :FoundEdition
18 | )
19 | )
20 |
21 | ECHO [ERROR] Could not find DisableOutOfProcBuild.exe in any known VS2022 edition.
22 | EXIT /B 1
23 |
24 | :FoundEdition
25 | ECHO Found Visual Studio 2022 Edition: %vs_edition%
26 | CD /D "%buildfix_path%"
27 | CALL DisableOutOfProcBuild.exe
28 |
29 | REM Restore previous directory
30 | CD /D "%current_path%"
31 |
32 | REM === Validate required files ===
33 | ECHO:
34 | ECHO === Checking required files ===
35 |
36 | SET "error=0"
37 |
38 | IF NOT EXIST "UnityLauncherPro\bin\Release\UnityLauncherPro.exe" (
39 | ECHO [ERROR] Missing file: UnityLauncherPro\bin\Release\UnityLauncherPro.exe
40 | SET "error=1"
41 | )
42 | IF NOT EXIST "UnityLauncherPro\Images\icon.ico" (
43 | ECHO [ERROR] Missing file: UnityLauncherPro\Images\icon.ico
44 | SET "error=1"
45 | )
46 |
47 | IF %error% NEQ 0 (
48 | ECHO [ERROR] Required files are missing. Aborting installer build.
49 | EXIT /B 1
50 | )
51 |
52 |
53 | ECHO:
54 | ECHO === Building Installer ===
55 | "%devenv_path%" UnityLauncherPro.sln /Rebuild Release /Project UnityLauncherProInstaller > build_output.log 2>&1
56 | SET "exitCode=%ERRORLEVEL%"
57 |
58 | TYPE build_output.log
59 | ECHO:
60 | ECHO === devenv.exe exit code: %exitCode% ===
61 |
62 | IF NOT "%exitCode%"=="0" (
63 | ECHO [ERROR] Installer build failed. Check build_output.log for details.
64 | EXIT /B %exitCode%
65 | )
66 |
67 |
68 | ECHO:
69 | ECHO === Build Complete ===
70 |
71 | REM Optional cleanup: disable workaround
72 | REG DELETE "HKCU\Software\Microsoft\VisualStudio\Setup" /v VSDisableOutOfProcBuild /f >NUL 2>&1
73 |
74 | ENDLOCAL
75 | EXIT /B %exitCode%
76 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to UnityLauncherPro
2 |
3 | Thanks for your interest in contributing!
4 | Here’s how you can help:
5 |
6 | ## How to Contribute
7 |
8 | 0. **Open Issue** Describe your idea first (if its useful, it might get accepted) OR check existing issues and comment there
9 | 1. **Fork** the repository
10 | 2. **Clone** your fork: `git clone https://github.com/YOURUSERNAME/UnityLauncherPro.git`
11 | 3. Optional: **Create a new branch**: `git checkout -b feature-name`
12 | 4. **Make your changes**
13 | 5. **Commit**: `git commit -m "Add feature XYZ"`
14 | 6. **Push**: `git push origin feature-name`
15 | 7. **Open a Pull Request** from your fork
16 |
17 | ## Reporting Bugs
18 |
19 | - Use the **Issues** tab
20 | - Include **steps to reproduce**, screenshots, and error messages
21 |
22 | ## Suggesting Features
23 |
24 | - Open an issue with `[Feature]` in the title
25 | - Describe the problem your feature solves
26 | - Note: Not all features get merged! *i'd like to keep workflows and UI something close to my own preferences (Sorry!), but you can of course make your own forks with additional features!
27 |
28 | ## Code Style & External Libraries
29 |
30 | - Style is not enforced
31 | - Simple code is good and functions can be as long as they need, to do the task (but can extract commonly used parts/methods into Tools or other helper libraries)
32 | - Avoid using external packages/nugets/dependencies (to keep this as a small single exe build)
33 | - Try to keep main features fast (or even improve existing features to make this more lightweight and faster to start etc.)
34 |
35 | ## Need Help?
36 |
37 | Feel free to open an issue or post in discussion.
38 |
39 | ---
40 |
41 | Thanks for contributing!
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 mika
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Paths.cmd.template:
--------------------------------------------------------------------------------
1 | @echo off
2 | REM Set your Visual Studio path here.
3 | SET VISUAL_STUDIO_PATH="%programfiles(x86)%\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe"
4 | @echo on
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UnityLauncherPro
2 | [](https://github.com/unitycoder/UnityLauncherPro/releases/latest/download/UnityLauncherPro.zip) [](https://github.com/unitycoder/UnityLauncherPro/releases/latest/download/UnityLauncherPro.zip) [](https://github.com/unitycoder/UnityLauncherPro/blob/master/LICENSE) [](https://discord.gg/cXT97hU)
[](https://www.virustotal.com/gui/url/e123b616cf4cbe3d3f7ba13b0d88cf5fff4638f72d5b9461088d0b11e9a41de3?nocache=1) [](https://github.com/unitycoder/UnityLauncherPro/actions/workflows/codeql.yml)
3 |
4 | Handle all your Unity versions and Projects easily!
5 |
6 | ## Features
7 | - Automagically Open Projects with Correct Unity Version
8 | - Display Recent Projects list with last modified date and project version info
9 | - List more than 40 recent projects!
10 | - Quickly Explore Project Folders
11 | - List installed Unity versions, can easily Run, Explore installation folder, View release notes
12 | - Download Missing Unity Versions in Browser
13 | - Can be used from commandline `UnityLauncherPro.exe -projectPath "c:/project/path/"`
14 | - Explorer context menu integration to launch project from folder
15 | - Use custom commandline launcher arguments per project (optional)
16 | - Show project git branch info (optional)
17 | - Current platform display and selection
18 | - Show list of released Unity versions/updates and view release notes page
19 | - 1-Click create new project with selected Unity version
20 | - Option to show missing projects in the list (if folder was removed)
21 | - 1-Click Start ADB Logcat with colors in command prompt
22 | - 1-Click browse editor log folder, crash logs folder, browse asset downloads folder, browse player logs folder
23 | - Quick Unity Editor Process Kill (press ALT+Q in the selected project row or right click context menu)
24 | - 1-Click start web server and launch WebGL build in browser for selected project
25 | - Custom skin color themes https://github.com/unitycoder/UnityLauncherPro/wiki/Custom-themes
26 | - Select template for new project (template based on selected unity version)
27 | - And more: https://github.com/unitycoder/UnityLauncherPro/wiki/Launcher-Comparison
28 |
29 | ### Powered by
30 | [](https://jb.gg/OpenSourceSupport)
31 |
32 | ### Forum Thread
33 | https://forum.unity.com/threads/unity-launcher-launch-correct-unity-versions-for-each-project-automatically.488718/
34 |
35 | ### Instructions
36 | https://github.com/unitycoder/UnityLauncherPro/wiki
37 |
38 | ### Development
39 | See DEV branch for latest commits https://github.com/unitycoder/UnityLauncherPro/tree/dev
40 | Pre-releases are sometimes available from dev branch: https://github.com/unitycoder/UnityLauncherPro/releases
41 |
42 | ### Contributing
43 | See https://github.com/unitycoder/UnityLauncherPro/blob/master/CONTRIBUTING.md
44 |
45 | ### Screenshots
46 |
47 | 
48 |
49 | 
50 |
51 | 
52 |
53 | 
54 |
55 | 
56 |
57 | ### Perform tasks on selected project
58 | 
59 |
60 | ### Quick New Project Creation (with Unity version and Templates selection)
61 | 
62 |
63 | ### Upgrade Project Version (automatically suggests next higher version)
64 | 
65 |
66 | ### Explorer integration (1-click opening projects with correct unity version)
67 | 
68 |
69 | ### View and Select current platform
70 | 
71 |
72 | ### Rename Project title
73 | 
74 |
75 |
76 | Old (winforms) version is here: https://github.com/unitycoder/UnityLauncher
77 |
78 |
79 |
--------------------------------------------------------------------------------
/UnityLauncherPro.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.13.35931.197
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityLauncherPro", "UnityLauncherPro\UnityLauncherPro.csproj", "{EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}"
7 | EndProject
8 | Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "UnityLauncherProInstaller", "UnityLauncherProInstaller\UnityLauncherProInstaller.vdproj", "{249180EF-BFC1-9DD7-48CC-E2B9253A64E0}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {249180EF-BFC1-9DD7-48CC-E2B9253A64E0}.Debug|Any CPU.ActiveCfg = Debug
21 | {249180EF-BFC1-9DD7-48CC-E2B9253A64E0}.Release|Any CPU.ActiveCfg = Release
22 | {249180EF-BFC1-9DD7-48CC-E2B9253A64E0}.Release|Any CPU.Build.0 = Release
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {CD70E364-F81A-402C-A387-1BEB396796A2}
29 | EndGlobalSection
30 | GlobalSection(Performance) = preSolution
31 | HasPerformanceSessions = true
32 | EndGlobalSection
33 | GlobalSection(Performance) = preSolution
34 | HasPerformanceSessions = true
35 | EndGlobalSection
36 | EndGlobal
37 |
--------------------------------------------------------------------------------
/UnityLauncherPro/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 600
18 |
19 |
20 | 650
21 |
22 |
23 | True
24 |
25 |
26 | False
27 |
28 |
29 | False
30 |
31 |
32 | True
33 |
34 |
35 | False
36 |
37 |
38 | False
39 |
40 |
41 | False
42 |
43 |
44 | False
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | True
54 |
55 |
56 | False
57 |
58 |
59 | False
60 |
61 |
62 | False
63 |
64 |
65 |
66 |
67 |
68 | False
69 |
70 |
71 | theme.ini
72 |
73 |
74 | False
75 |
76 |
77 | False
78 |
79 |
80 | False
81 |
82 |
83 | dd/MM/yyyy HH:mm:ss
84 |
85 |
86 | False
87 |
88 |
89 | False
90 |
91 |
92 | -s Unity ActivityManager PackageManager dalvikvm DEBUG -v color
93 |
94 |
95 | 0
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | False
105 |
106 |
107 | False
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | C:\Program Files\
116 |
117 |
118 |
119 |
120 | InitializeProject.cs
121 |
122 |
123 | False
124 |
125 |
126 | 50000
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | False
135 |
136 |
137 | 40
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/UnityLauncherPro/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace UnityLauncherPro
4 | {
5 | ///
6 | /// Interaction logic for App.xaml
7 | ///
8 | public partial class App : Application
9 | {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Converters/LastModifiedConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace UnityLauncherPro.Converters
6 | {
7 | // https://stackoverflow.com/a/14283973/5452781
8 | [ValueConversion(typeof(DateTime), typeof(String))]
9 | public class LastModifiedConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | // TODO: without this, editor mode fails with null references.. but would be nice to get rid of if's..
14 | if (value == null) return null;
15 |
16 | DateTime date = (DateTime)value;
17 |
18 | return MainWindow.useHumanFriendlyDateFormat ? Tools.GetElapsedTime(date) : date.ToString(MainWindow.currentDateFormat);
19 | }
20 |
21 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
22 | {
23 | return DateTime.ParseExact((string)value, MainWindow.currentDateFormat, culture);
24 | }
25 |
26 | }
27 |
28 | // just for tooltip
29 | [ValueConversion(typeof(DateTime), typeof(String))]
30 | public class LastModifiedConverterTooltip : IValueConverter
31 | {
32 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
33 | {
34 | if (value == null) return null;
35 | DateTime date = (DateTime)value;
36 | return date.ToString(MainWindow.currentDateFormat);
37 | }
38 |
39 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
40 | {
41 | return DateTime.ParseExact((string)value, MainWindow.currentDateFormat, culture);
42 | }
43 |
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Converters/ReleaseDateConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows;
4 | using System.Windows.Data;
5 |
6 | namespace UnityLauncherPro.Converters
7 | {
8 | [ValueConversion(typeof(DateTime), typeof(string))]
9 | public class ReleaseDateConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | if (value == null || !(value is DateTime date))
14 | {
15 | return DependencyProperty.UnsetValue;
16 | }
17 |
18 | // Use a default date format if currentDateFormat is null or empty
19 | string dateStrTrimmed = MainWindow.currentDateFormat ?? "MM/dd/yyyy";
20 |
21 | // If the format includes time, use only the date portion
22 | if (dateStrTrimmed.Contains(" "))
23 | {
24 | dateStrTrimmed = dateStrTrimmed.Split(' ')[0];
25 | }
26 |
27 | // Return a human-friendly format if enabled; otherwise, format based on dateStrTrimmed
28 | return MainWindow.useHumanFriendlyDateFormat
29 | ? Tools.GetElapsedTime(date)
30 | : date.ToString(dateStrTrimmed, culture);
31 | }
32 |
33 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
34 | {
35 | if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
36 | {
37 | return DependencyProperty.UnsetValue;
38 | }
39 |
40 | // Attempt to parse back to DateTime using the specified format
41 | if (DateTime.TryParseExact((string)value, MainWindow.currentDateFormat ?? "MM/dd/yyyy", culture, DateTimeStyles.None, out DateTime parsedDate))
42 | {
43 | return parsedDate;
44 | }
45 |
46 | return DependencyProperty.UnsetValue;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/BuildReport.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace UnityLauncherPro
4 | {
5 | public class BuildReport
6 | {
7 | public long ElapsedTimeMS { set; get; }
8 | public List Stats { set; get; } // overal per category sizes
9 | public List Items { set; get; } // report rows
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/BuildReportItem.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro
2 | {
3 | public class BuildReportItem
4 | {
5 | // TODO use real values, so can sort and convert kb/mb
6 | public string Category { set; get; } // for category list
7 | public string Size { set; get; }
8 | public string Percentage { set; get; }
9 | public string Path { set; get; }
10 | public string Format { set; get; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/DownloadProgress.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro
2 | {
3 | public readonly struct DownloadProgress
4 | {
5 | public long TotalRead { get; }
6 | public long TotalBytes { get; }
7 |
8 | public DownloadProgress(long totalRead, long totalBytes)
9 | {
10 | TotalRead = totalRead;
11 | TotalBytes = totalBytes;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/MessageType.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro.Data
2 | {
3 | public enum MessageType
4 | {
5 | Info,
6 | Warning,
7 | Error
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/Platform.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro
2 | {
3 | // TODO should display only valid platforms for projects Unity version
4 | public enum Platform
5 | {
6 | Unknown,
7 | Win32, Win64, OSX, Linux, Linux64, iOS, Android, Web, WebStreamed, WebGL, Xboxone, OS4, PSP2, WSAPlayer, Tizen, SamsungTV,
8 | Standalone, Win, OSXuniversal, LinuxUniversal, WindowsStoreApps, @Switch, Wiiu, N3DS, tVoS, PSM,
9 | // manually added
10 | StandaloneWindows, StandaloneWindows64
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/Project.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 |
5 | namespace UnityLauncherPro
6 | {
7 | public class Project : IValueConverter
8 | {
9 | public string Title { set; get; }
10 | public string Version { set; get; }
11 | public string Path { set; get; }
12 | public DateTime? Modified { set; get; }
13 | public string Arguments { set; get; }
14 | public string GITBranch { set; get; } // TODO rename to Branch
15 | //public string TargetPlatform { set; get; }
16 | public string TargetPlatform { set; get; } // TODO rename to Platform
17 | public string[] TargetPlatforms { set; get; }
18 | public bool folderExists { set; get; }
19 | public string SRP { set; get; } // Scriptable Render Pipeline, TODO add version info?
20 |
21 | // WPF keeps calling this method from AppendFormatHelper, GetNameCore..? not sure if need to return something else or default would be faster?
22 | public override string ToString()
23 | {
24 | return Path;
25 | }
26 |
27 | // for debugging
28 | //public override string ToString()
29 | //{
30 | // return $"{Title} {Version} {Path} {Modified} {Arguments} {GITBranch} {TargetPlatform}";
31 | //}
32 |
33 | // change datagrid colors based on value using converter https://stackoverflow.com/a/5551986/5452781
34 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
35 | {
36 | bool b = (bool)value;
37 | return b;
38 | }
39 |
40 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
41 | {
42 | throw new NotSupportedException();
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/Tabs.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro
2 | {
3 | public enum Tabs
4 | {
5 | Projects = 0,
6 | Unitys = 1,
7 | Updates = 2,
8 | Settings = 3
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/ThemeColor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Windows.Data;
4 | using System.Windows.Media;
5 |
6 | namespace UnityLauncherPro
7 | {
8 | public class ThemeColor : IValueConverter
9 | {
10 | public string Key { get; set; }
11 | public SolidColorBrush Brush { get; set; }
12 |
13 |
14 | object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
15 | {
16 | return (SolidColorBrush)value;
17 | }
18 |
19 | object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
20 | {
21 | throw new NotImplementedException();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/UnityInstallation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Data;
3 |
4 | namespace UnityLauncherPro
5 | {
6 | public class UnityInstallation : IValueConverter
7 | {
8 | public string Version { set; get; }
9 | public long VersionCode { set; get; } // version as number, cached for sorting
10 | public string Path { set; get; } // exe path
11 | public DateTime? Installed { set; get; }
12 | public string PlatformsCombined { set; get; }
13 | public string[] Platforms { set; get; }
14 | public int ProjectCount { set; get; }
15 | public bool IsPreferred { set; get; }
16 | public string ReleaseType { set; get; } // Alpha, Beta, LTS.. TODO could be enum
17 |
18 | // https://stackoverflow.com/a/5551986/5452781
19 | public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
20 | {
21 | string version = value as string;
22 | if (string.IsNullOrEmpty(version)) return null;
23 | return MainWindow.unityInstalledVersions.ContainsKey(version);
24 | }
25 |
26 | public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
27 | {
28 | throw new NotSupportedException();
29 | }
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/UnityVersion.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace UnityLauncherPro
5 | {
6 | public class UnityVersion
7 | {
8 | public string Version { get; set; }
9 | public UnityVersionStream Stream { get; set; }
10 | public DateTime ReleaseDate { get; set; }
11 | public string directURL { get; set; } = null; // if found from unofficial releases list
12 |
13 | public static UnityVersion FromJson(string json)
14 | {
15 | var values = ParseJsonToDictionary(json);
16 |
17 | return new UnityVersion
18 | {
19 | Version = values.ContainsKey("version") ? values["version"] : null,
20 | Stream = ParseStream(values.ContainsKey("stream") ? values["stream"] : null),
21 | ReleaseDate = DateTime.TryParse(values.ContainsKey("releaseDate") ? values["releaseDate"] : null, out var date)
22 | ? date
23 | : default
24 | };
25 | }
26 |
27 | public string ToJson()
28 | {
29 | return $"{{ \"version\": \"{Version}\", \"stream\": \"{Stream}\", \"releaseDate\": \"{ReleaseDate:yyyy-MM-ddTHH:mm:ss}\" }}";
30 | }
31 |
32 | private static Dictionary ParseJsonToDictionary(string json)
33 | {
34 | var result = new Dictionary();
35 | json = json.Trim(new char[] { '{', '}', ' ' });
36 | var keyValuePairs = json.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
37 |
38 | foreach (var pair in keyValuePairs)
39 | {
40 | var keyValue = pair.Split(new[] { ':' }, 2);
41 | if (keyValue.Length == 2)
42 | {
43 | var key = keyValue[0].Trim(new char[] { ' ', '"' });
44 | var value = keyValue[1].Trim(new char[] { ' ', '"' });
45 | result[key] = value;
46 | }
47 | }
48 |
49 | return result;
50 | }
51 |
52 | private static UnityVersionStream ParseStream(string stream)
53 | {
54 | return Enum.TryParse(stream, true, out UnityVersionStream result) ? result : UnityVersionStream.Tech;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/UnityVersionResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace UnityLauncherPro
4 | {
5 | public class UnityVersionResponse
6 | {
7 | public int Offset { get; set; }
8 | public int Limit { get; set; }
9 | public int Total { get; set; }
10 | public List Results { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/Data/UnityVersionStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace UnityLauncherPro
5 | {
6 | public enum UnityVersionStream
7 | {
8 | Alpha,
9 | Beta,
10 | LTS,
11 | Tech
12 | }
13 |
14 | public class UnityVersionJSON
15 | {
16 | public string Version { get; set; }
17 | public DateTime ReleaseDate { get; set; }
18 | public UnityVersionStream Stream { get; set; }
19 | public List Downloads { get; set; }
20 | public string ShortRevision { get; set; }
21 | }
22 |
23 | public class Download
24 | {
25 | public string Url { get; set; }
26 | public string Type { get; set; }
27 | public string Platform { get; set; }
28 | public string Architecture { get; set; }
29 | public DownloadSize DownloadSize { get; set; }
30 | public List Modules { get; set; }
31 | }
32 |
33 | public class DownloadSize
34 | {
35 | public long Value { get; set; }
36 | public string Unit { get; set; }
37 | }
38 |
39 | public class Module
40 | {
41 | public string Id { get; set; }
42 | public string Name { get; set; }
43 | public string Description { get; set; }
44 | public string Url { get; set; }
45 | }
46 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/DownloadProgressWindow.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/UnityLauncherPro/DownloadProgressWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Input;
4 |
5 | namespace UnityLauncherPro
6 | {
7 | public partial class DownloadProgressWindow
8 | {
9 | private readonly Action _cancelAction;
10 | private readonly string _subjectName;
11 | private static MainWindow MainWindow => Tools.mainWindow;
12 |
13 | public DownloadProgressWindow(string subjectName, Action cancelAction = null)
14 | {
15 | InitializeComponent();
16 | _subjectName = subjectName;
17 | Title = subjectName;
18 | _cancelAction = cancelAction;
19 | Topmost = true;
20 | Owner = MainWindow;
21 | WindowStartupLocation = WindowStartupLocation.CenterOwner;
22 | MainWindow.IsEnabled = false;
23 | }
24 |
25 | public void UpdateProgress(DownloadProgress downloadProgress)
26 | {
27 | Title = $"Downloading {_subjectName} ({downloadProgress.TotalRead / 1024d / 1024d:F1} MB / {downloadProgress.TotalBytes / 1024d / 1024d:F1} MB)";
28 | var progress = downloadProgress.TotalBytes == 0 ? 0 : downloadProgress.TotalRead * 100d / downloadProgress.TotalBytes;
29 | ProgressBar.Value = progress;
30 | ProgressText.Text = $"{progress / 100:P1}";
31 | }
32 |
33 | private void CancelDownloadClick(object sender, RoutedEventArgs e)
34 | {
35 | CancelDownload();
36 | }
37 |
38 | private void CancelDownload()
39 | {
40 | _cancelAction?.Invoke();
41 | }
42 |
43 | private void Window_PreviewLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
44 | {
45 | var window = (Window)sender;
46 | window.Topmost = true;
47 | }
48 |
49 | protected override void OnClosed(EventArgs e)
50 | {
51 | base.OnClosed(e);
52 | _cancelAction?.Invoke();
53 | MainWindow.IsEnabled = true;
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/GetProjects.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Win32;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Collections.Specialized;
5 | using System.IO;
6 | using System.Text;
7 |
8 | namespace UnityLauncherPro
9 | {
10 | public static class GetProjects
11 | {
12 | // which registries we want to scan for projects
13 | static readonly string[] registryPathsToCheck = new string[] { @"SOFTWARE\Unity Technologies\Unity Editor 5.x", @"SOFTWARE\Unity Technologies\Unity Editor 4.x" };
14 |
15 | // convert target platform name into valid buildtarget platform name, NOTE this depends on unity version, now only 2019 and later are supported
16 | public static Dictionary remapPlatformNames = new Dictionary { { "StandaloneWindows64", "Win64" }, { "StandaloneWindows", "Win" }, { "Android", "Android" }, { "WebGL", "WebGL" } };
17 |
18 | public static List Scan(bool getGitBranch = false, bool getPlasticBranch = false, bool getArguments = false, bool showMissingFolders = false, bool showTargetPlatform = false, StringCollection AllProjectPaths = null, bool searchGitbranchRecursively = false, bool showSRP = false)
19 | {
20 | List projectsFound = new List();
21 |
22 | var hklm = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);
23 |
24 | // check each version path
25 | for (int i = 0, len = registryPathsToCheck.Length; i < len; i++)
26 | {
27 | RegistryKey key = hklm.OpenSubKey(registryPathsToCheck[i]);
28 |
29 | if (key == null)
30 | {
31 | continue;
32 | }
33 | else
34 | {
35 | //Console.WriteLine("Null registry key at " + registryPathsToCheck[i]);
36 | }
37 |
38 | // parse recent project path
39 | foreach (var valueName in key.GetValueNames())
40 | {
41 | if (valueName.IndexOf("RecentlyUsedProjectPaths-") == 0)
42 | {
43 | string projectPath = "";
44 | // check if binary or not
45 | var valueKind = key.GetValueKind(valueName);
46 | if (valueKind == RegistryValueKind.Binary)
47 | {
48 | byte[] projectPathBytes = (byte[])key.GetValue(valueName);
49 | projectPath = Encoding.UTF8.GetString(projectPathBytes, 0, projectPathBytes.Length - 1);
50 | }
51 | else // should be string then
52 | {
53 | projectPath = (string)key.GetValue(valueName);
54 | }
55 |
56 | var p = GetProjectInfo(projectPath, getGitBranch, getPlasticBranch, getArguments, showMissingFolders, showTargetPlatform, searchGitbranchRecursively, showSRP);
57 | //Console.WriteLine(projectPath+" "+p.TargetPlatform);
58 |
59 | // if want to hide project and folder path for screenshot
60 | //p.Title = "Example Project";
61 | //p.Path = "C:/Projects/MyProj";
62 |
63 | if (p != null)
64 | {
65 | projectsFound.Add(p);
66 |
67 | // add found projects to history also (gets added only if its not already there)
68 | Tools.AddProjectToHistory(p.Path);
69 | }
70 | } // valid key
71 | } // each key
72 | } // for each registry root
73 |
74 | // NOTE those 40 projects should be added to custom list, otherwise they will disappear (since last item is not yet added to our list, until its launched once, so need to launch many projects, to start collecting history..)
75 | // but then we would have to loop here again..? or add in the loop above..if doesnt exists on list, and the remove extra items from the end
76 |
77 | // scan info for custom folders (if not already on the list)
78 | if (AllProjectPaths != null)
79 | {
80 | // iterate custom full projects history
81 | foreach (var projectPath in AllProjectPaths)
82 | {
83 | // check if registry list contains this path already, then skip it
84 | bool found = false;
85 | for (int i = 0, len = projectsFound.Count; i < len; i++)
86 | {
87 | if (projectsFound[i].Path == projectPath)
88 | {
89 | found = true;
90 | break;
91 | }
92 | }
93 |
94 | // if not found from registry, add to recent projects list
95 | if (found == false)
96 | {
97 | var p = GetProjectInfo(projectPath, getGitBranch, getPlasticBranch, getArguments, showMissingFolders, showTargetPlatform, searchGitbranchRecursively, showSRP);
98 | if (p != null) projectsFound.Add(p);
99 | }
100 | }
101 | }
102 |
103 | // sometimes projects are in wrong order, seems to be related to messing up your unity registry, the keys are received in created order (so if you had removed/modified them manually, it might return wrong order instead of 0 - 40)
104 | // sort by modified date, projects without modified date are put to last, NOTE: this would remove projects without modified date (if they become last items, before trimming list on next step)
105 | projectsFound.Sort((x, y) =>
106 | {
107 | if (x.Modified == null && y.Modified == null) return 0; // cannot be -1, https://stackoverflow.com/a/42821992/5452781
108 | if (x.Modified == null) return 1;
109 | if (y.Modified == null) return -1;
110 | return y.Modified.Value.CompareTo(x.Modified.Value);
111 | //return x.Modified.Value.CompareTo(y.Modified.Value); // BUG this breaks something, so that last item platform is wrong (for project that is missing from disk) ?
112 | });
113 |
114 | // trim list to max amount (older ones are dropped)
115 | if (projectsFound.Count > MainWindow.maxProjectCount)
116 | {
117 | //Console.WriteLine("Trimming projects list to " + MainWindow.maxProjectCount + " projects");
118 | projectsFound.RemoveRange(MainWindow.maxProjectCount, projectsFound.Count - MainWindow.maxProjectCount);
119 | }
120 | return projectsFound;
121 | } // Scan()
122 |
123 | static Project GetProjectInfo(string projectPath, bool getGitBranch = false, bool getPlasticBranch = false, bool getArguments = false, bool showMissingFolders = false, bool showTargetPlatform = false, bool searchGitbranchRecursively = false, bool showSRP = false)
124 | {
125 | bool folderExists = Directory.Exists(projectPath);
126 |
127 | // if displaying missing folders are disabled, and folder doesnt exists, skip this project
128 | if (showMissingFolders == false && folderExists == false) return null;
129 | string projectName = "";
130 |
131 | // get project name from full path
132 | if (projectPath.IndexOf(Path.DirectorySeparatorChar) > -1)
133 | {
134 | projectName = projectPath.Substring(projectPath.LastIndexOf(Path.DirectorySeparatorChar) + 1);
135 | }
136 | else if (projectPath.IndexOf(Path.AltDirectorySeparatorChar) > -1)
137 | {
138 | projectName = projectPath.Substring(projectPath.LastIndexOf(Path.AltDirectorySeparatorChar) + 1);
139 | }
140 | else // no path separator found
141 | {
142 | projectName = projectPath;
143 | }
144 |
145 | //Console.WriteLine("valueName="+ valueName+" , projectName =" + projectName);
146 |
147 | // get last modified date from most recent date of solution or folder
148 | string solutionPath = Path.Combine(projectPath, $"{projectName}.sln");
149 | bool solutionExists = File.Exists(solutionPath);
150 | DateTime? solutionLastUpdated = solutionExists ? Tools.GetLastModifiedTime(solutionPath) : null;
151 | DateTime? folderLastUpdated = folderExists ? Tools.GetLastModifiedTime(projectPath) : null;
152 | DateTime? lastUpdated = solutionLastUpdated > folderLastUpdated ? solutionLastUpdated : folderLastUpdated;
153 |
154 | // get project version
155 | string projectVersion = folderExists ? Tools.GetProjectVersion(projectPath) : null;
156 |
157 | // get custom launch arguments, only if column in enabled
158 | string customArgs = "";
159 | if (getArguments == true)
160 | {
161 | customArgs = folderExists ? Tools.ReadCustomProjectData(projectPath, MainWindow.launcherArgumentsFile) : null;
162 | }
163 |
164 | // get git branchinfo, only if column in enabled
165 | string gitBranch = "";
166 | if (getGitBranch == true)
167 | {
168 | gitBranch = folderExists ? Tools.ReadGitBranchInfo(projectPath, searchGitbranchRecursively) : null;
169 |
170 | // check for plastic, if enabled
171 | if (getPlasticBranch == true && gitBranch == null)
172 | {
173 | gitBranch = folderExists ? Tools.ReadPlasticBranchInfo(projectPath, searchGitbranchRecursively) : null;
174 | }
175 | }
176 |
177 | string targetPlatform = null;
178 | if (showTargetPlatform == true)
179 | {
180 | targetPlatform = folderExists ? Tools.GetTargetPlatform(projectPath) : null;
181 | //if (projectPath.Contains("Shader")) Console.WriteLine(projectPath + " targetPlatform=" + targetPlatform);
182 | }
183 | var p = new Project();
184 |
185 | switch (MainWindow.projectNameSetting)
186 | {
187 | case 0:
188 | p.Title = Tools.ReadCustomProjectData(projectPath, MainWindow.projectNameFile);
189 | break;
190 | case 1:
191 | p.Title = Tools.ReadProjectName(projectPath);
192 | break;
193 | default:
194 | p.Title = projectName;
195 | break;
196 | }
197 |
198 | // if no custom data or no product name found
199 | if (string.IsNullOrEmpty(p.Title)) p.Title = projectName;
200 |
201 | p.Version = projectVersion;
202 | p.Path = projectPath;
203 | p.Modified = lastUpdated;
204 | p.Arguments = customArgs;
205 | p.GITBranch = gitBranch;
206 | //Console.WriteLine("targetPlatform " + targetPlatform + " projectPath:" + projectPath);
207 | p.TargetPlatform = targetPlatform;
208 |
209 | // bubblegum(TM) solution, fill available platforms for this unity version, for this project
210 | p.TargetPlatforms = Tools.GetPlatformsForUnityVersion(projectVersion);
211 | p.folderExists = folderExists;
212 |
213 | if (showSRP == true) p.SRP = Tools.GetSRP(projectPath);
214 |
215 | return p;
216 | }
217 |
218 | // removes project from unity registry recent projects, OR if its our custom list, remove from there
219 | public static bool RemoveRecentProject(string projectPathToRemove)
220 | {
221 | bool result = false;
222 |
223 | var hklm = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64);
224 |
225 | // check each version path
226 | for (int i = 0, len = registryPathsToCheck.Length; i < len; i++)
227 | {
228 | RegistryKey key = hklm.OpenSubKey(registryPathsToCheck[i], true);
229 |
230 | if (key == null)
231 | {
232 | continue;
233 | }
234 | else
235 | {
236 | //Console.WriteLine("Null registry key at " + registryPathsToCheck[i]);
237 | }
238 |
239 | // parse recent project paths
240 | foreach (var valueName in key.GetValueNames())
241 | {
242 | if (valueName.IndexOf("RecentlyUsedProjectPaths-") == 0)
243 | {
244 | string projectPath = "";
245 | // check if binary or not
246 | var valueKind = key.GetValueKind(valueName);
247 | if (valueKind == RegistryValueKind.Binary)
248 | {
249 | byte[] projectPathBytes = (byte[])key.GetValue(valueName);
250 | projectPath = Encoding.UTF8.GetString(projectPathBytes, 0, projectPathBytes.Length - 1);
251 | }
252 | else // should be string then
253 | {
254 | projectPath = (string)key.GetValue(valueName);
255 | }
256 |
257 | // if our project folder, remove registry item
258 | if (projectPath == projectPathToRemove)
259 | {
260 | Console.WriteLine("Deleted registery item: " + valueName + " projectPath=" + projectPath);
261 | key.DeleteValue(valueName);
262 | //return true;
263 | result = true;
264 | // TODO should break both loops
265 | //break;
266 | goto jumpCustomHistory;
267 | }
268 |
269 | } // valid key
270 | } // each key
271 | } // for each registry root
272 |
273 | jumpCustomHistory:
274 | // check if its in our custom list, NOTE should only do if custom list is enabled
275 | if (Properties.Settings.Default.projectPaths.Contains(projectPathToRemove))
276 | {
277 | Properties.Settings.Default.projectPaths.Remove(projectPathToRemove);
278 | Properties.Settings.Default.Save();
279 | Console.WriteLine("Deleted recent history item: " + projectPathToRemove + " from custom list");
280 | //return true;
281 | result = true;
282 | }
283 |
284 |
285 | return result;
286 | } // RemoveRecentProject()
287 |
288 | } // class
289 | } // namespace
290 |
291 |
--------------------------------------------------------------------------------
/UnityLauncherPro/GetUnityInstallations.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace UnityLauncherPro
6 | {
7 | ///
8 | /// returns unity installations under given root folders
9 | ///
10 | public static class GetUnityInstallations
11 | {
12 | static Dictionary platformNames = new Dictionary { { "androidplayer", "Android" }, { "windowsstandalonesupport", "Win" }, { "linuxstandalonesupport", "Linux" }, { "LinuxStandalone", "Linux" }, { "OSXStandalone", "OSX" }, { "webglsupport", "WebGL" }, { "metrosupport", "UWP" }, { "iossupport", "iOS" } };
13 |
14 |
15 | // returns unity installations
16 | public static List Scan()
17 | {
18 | // get list from settings
19 | var rootFolders = Properties.Settings.Default.rootFolders;
20 |
21 | // unityversion, exe_path
22 | List results = new List();
23 |
24 | // iterate all folders under root folders
25 | foreach (string rootFolder in rootFolders)
26 | {
27 | // if folder exists
28 | if (String.IsNullOrWhiteSpace(rootFolder) == true || Directory.Exists(rootFolder) == false) continue;
29 |
30 | // get all folders
31 | var directories = Directory.GetDirectories(rootFolder);
32 | // parse all folders under root, and search for unity editor files
33 | for (int i = 0, length = directories.Length; i < length; i++)
34 | {
35 | var editorFolder = Path.Combine(directories[i], "Editor");
36 | if (Directory.Exists(editorFolder) == false)
37 | {
38 | // OPTIONAL scan for source code build output
39 | editorFolder = Path.Combine(directories[i], "build/WindowsEditor/x64/Release");
40 | if (Directory.Exists(editorFolder) == false)
41 | {
42 | // no unity editor root folder found, skip this folder
43 | continue;
44 | }
45 | }
46 |
47 | // check if uninstaller is there, sure sign of unity
48 | var uninstallExe = Path.Combine(editorFolder, "Uninstall.exe");
49 | var haveUninstaller = File.Exists(uninstallExe);
50 |
51 | var exePath = Path.Combine(editorFolder, "Unity.exe");
52 | if (File.Exists(exePath) == false) continue;
53 |
54 | // get full version number from uninstaller (or try exe, if no uninstaller)
55 | var version = Tools.GetFileVersionData(haveUninstaller ? uninstallExe : exePath);
56 |
57 | // we got new version to add
58 | var dataFolder = Path.Combine(editorFolder, "Data");
59 | DateTime? installDate = Tools.GetLastModifiedTime(dataFolder);
60 | UnityInstallation unity = new UnityInstallation();
61 | unity.Version = version;
62 | unity.VersionCode = Tools.VersionAsLong(version); // cached version code
63 | unity.Path = exePath;
64 | unity.Installed = installDate;
65 | unity.IsPreferred = (version == MainWindow.preferredVersion);
66 | unity.ProjectCount = GetProjectCountForUnityVersion(version);
67 |
68 | if (Tools.IsAlpha(version))
69 | {
70 | unity.ReleaseType = "Alpha";
71 | }
72 | else if (Tools.IsBeta(version))
73 | {
74 | unity.ReleaseType = "Beta";
75 | }
76 | else
77 | if (Tools.IsLTS(version))
78 |
79 | {
80 | unity.ReleaseType = "LTS";
81 | }
82 | else
83 | {
84 | unity.ReleaseType = ""; // cannot be null for UnitysFilter to work properly
85 | }
86 |
87 | // get platforms, NOTE if this is slow, do it later, or skip for commandline
88 | var platforms = GetPlatforms(dataFolder);
89 | // this is for editor tab, show list of all platforms in cell
90 | if (platforms != null) unity.PlatformsCombined = string.Join(", ", platforms);
91 | // this is for keeping array of platforms for platform combobox
92 | if (platforms != null) unity.Platforms = platforms;
93 |
94 | // add to list, if not there yet NOTE should notify that there are 2 same versions..? this might happen with preview builds..
95 | if (results.Contains(unity) == true)
96 | {
97 | Console.WriteLine("Warning: 2 same versions found for " + version);
98 | continue;
99 | }
100 |
101 | results.Add(unity);
102 | } // got folders
103 | } // all root folders
104 |
105 | // sort by version
106 | results.Sort((s1, s2) => s2.VersionCode.CompareTo(s1.VersionCode));
107 |
108 | return results;
109 | } // scan()
110 |
111 | public static bool HasUnityInstallations(string path)
112 | {
113 | var directories = Directory.GetDirectories(path);
114 |
115 | // loop folders inside root
116 | for (int i = 0, length = directories.Length; i < length; i++)
117 | {
118 | var editorFolder = Path.Combine(directories[i], "Editor");
119 | if (Directory.Exists(editorFolder) == false) continue;
120 |
121 | var editorExe = Path.Combine(editorFolder, "Unity.exe");
122 | if (File.Exists(editorExe) == false) continue;
123 |
124 | // have atleast 1 installation
125 | return true;
126 | }
127 |
128 | return false;
129 | }
130 |
131 | // scans unity installation folder for installed platforms
132 | static string[] GetPlatforms(string dataFolder)
133 | {
134 | // get all folders inside
135 | var platformFolder = Path.Combine(dataFolder, "PlaybackEngines");
136 | if (Directory.Exists(platformFolder) == false) return null;
137 |
138 | //var directories = Directory.GetDirectories(platformFolder);
139 | var directories = new List(Directory.GetDirectories(platformFolder));
140 | //for (int i = 0; i < directories.Length; i++)
141 | var count = directories.Count;
142 | for (int i = 0; i < count; i++)
143 | {
144 | var foldername = Path.GetFileName(directories[i]).ToLower();
145 | //Console.WriteLine("PlaybackEngines: " + foldername);
146 | // check if have better name in dictionary
147 | if (platformNames.ContainsKey(foldername))
148 | {
149 | directories[i] = platformNames[foldername];
150 |
151 | // add also 64bit desktop versions for that platform, NOTE dont add android, ios or webgl
152 | if (foldername.IndexOf("alone") > -1) directories.Add(platformNames[foldername] + "64");
153 | }
154 | else // use raw
155 | {
156 | directories[i] = foldername;
157 | }
158 | //Console.WriteLine(i + " : " + foldername + " > " + directories[i]);
159 | }
160 |
161 | return directories.ToArray();
162 | }
163 |
164 | static int GetProjectCountForUnityVersion(string version)
165 | {
166 | if (MainWindow.projectsSource == null) return 0;
167 | //Console.WriteLine("xxx "+(MainWindow.projectsSource==null));
168 | int count = 0;
169 | // count projects using this exact version
170 | for (int i = 0, len = MainWindow.projectsSource.Count; i < len; i++)
171 | {
172 | if (MainWindow.projectsSource[i].Version == version) count++;
173 | }
174 | return count;
175 | }
176 |
177 | } // class
178 | } // namespace
179 |
--------------------------------------------------------------------------------
/UnityLauncherPro/GetUnityUpdates.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Threading.Tasks;
8 |
9 | namespace UnityLauncherPro
10 | {
11 | public static class GetUnityUpdates
12 | {
13 | private const string BaseApiUrl = "https://services.api.unity.com/unity/editor/release/v1/releases";
14 | private const int BatchSize = 25;
15 | private const int RequestsPerBatch = 10;
16 | private const int DelayBetweenBatches = 1000; // 1 second in milliseconds
17 | private const string CacheFileName = "UnityVersionCache.json";
18 | private static readonly HttpClient Client = new HttpClient();
19 |
20 | static Dictionary unofficialReleaseURLs = new Dictionary();
21 |
22 | public static async Task> FetchAll(bool useUnofficialList = false)
23 | {
24 | var cachedVersions = LoadCachedVersions();
25 | var newVersions = await FetchNewVersions(cachedVersions);
26 |
27 | var allVersions = newVersions.Concat(cachedVersions).ToList();
28 |
29 | if (useUnofficialList == true)
30 | {
31 | var unofficialVersions = await FetchUnofficialVersions(cachedVersions);
32 | unofficialReleaseURLs.Clear();
33 | // TODO modify FetchUnofficialVersions to put items in this dictionary directlys
34 | foreach (var version in unofficialVersions)
35 | {
36 | //Console.WriteLine("unofficial: " + version.Version + " , " + version.directURL);
37 | if (unofficialReleaseURLs.ContainsKey(version.Version) == false)
38 | {
39 | unofficialReleaseURLs.Add(version.Version, version.directURL);
40 | }
41 | }
42 | allVersions = unofficialVersions.Concat(allVersions).ToList();
43 | }
44 |
45 | if (newVersions.Count > 0 || (useUnofficialList && unofficialReleaseURLs.Count > 0))
46 | {
47 | SaveCachedVersions(allVersions);
48 | }
49 |
50 |
51 | return allVersions;
52 | }
53 |
54 | public static async Task> FetchUnofficialVersions(List cachedVersions)
55 | {
56 | var unofficialVersions = new List();
57 | var existingVersions = new HashSet(cachedVersions.Select(v => v.Version));
58 |
59 | try
60 | {
61 | string url = "https://raw.githubusercontent.com/unitycoder/UnofficialUnityReleasesWatcher/refs/heads/main/unity-releases.md";
62 |
63 | var content = await Client.GetStringAsync(url);
64 |
65 | // Parse the Markdown content
66 | var lines = content.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
67 | foreach (var line in lines)
68 | {
69 | if (line.StartsWith("- ")) // Identify Markdown list items
70 | {
71 | var urlPart = line.Substring(2).Trim();
72 | var version = ExtractVersionFromUrl(urlPart);
73 |
74 | if (!string.IsNullOrEmpty(version) && !existingVersions.Contains(version))
75 | {
76 | var stream = InferStreamFromVersion(version);
77 |
78 | unofficialVersions.Add(new UnityVersion
79 | {
80 | Version = version,
81 | Stream = stream,
82 | ReleaseDate = DateTime.Now, // NOTE not correct, but we don't have known release date for unofficial versions (its only when they are found..)
83 | //ReleaseDate = DateTime.MinValue // Release date is unavailable in the MD format, TODO add to md as #2021-01-01 ?
84 | directURL = urlPart, // this is available only for unofficial releases
85 | });
86 | }
87 | }
88 | }
89 | }
90 | catch (Exception ex)
91 | {
92 | Console.WriteLine($"Error fetching unofficial versions: {ex.Message}");
93 | }
94 |
95 | return unofficialVersions;
96 | }
97 |
98 | // TODO fixme, f is not always LTS
99 | private static UnityVersionStream InferStreamFromVersion(string version)
100 | {
101 | if (Tools.IsAlpha(version)) return UnityVersionStream.Alpha;
102 | if (Tools.IsBeta(version)) return UnityVersionStream.Beta;
103 | if (Tools.IsLTS(version)) return UnityVersionStream.LTS;
104 |
105 | //if (version.Contains("a")) return UnityVersionStream.Alpha;
106 | //if (version.Contains("b")) return UnityVersionStream.Beta;
107 | //if (version.Contains("f")) return UnityVersionStream.LTS;
108 | return UnityVersionStream.Tech; // Default to Tech if no identifier is found
109 | }
110 |
111 | ///
112 | /// Extracts the Unity version from the given URL.
113 | ///
114 | /// The URL to parse.
115 | /// The Unity version string.
116 | private static string ExtractVersionFromUrl(string url)
117 | {
118 | try
119 | {
120 | var versionStart = url.LastIndexOf('#') + 1;
121 | return versionStart > 0 && versionStart < url.Length ? url.Substring(versionStart) : null;
122 | }
123 | catch
124 | {
125 | return null;
126 | }
127 | }
128 |
129 | public static async Task FetchDownloadUrl(string unityVersion)
130 | {
131 | if (string.IsNullOrEmpty(unityVersion))
132 | {
133 | return null;
134 | }
135 |
136 | // unity release api
137 | string apiUrl = $"{BaseApiUrl}?limit=1&version={unityVersion}&architecture=X86_64&platform=WINDOWS";
138 |
139 | try
140 | {
141 | string responseString = await Client.GetStringAsync(apiUrl);
142 | return await ExtractDownloadUrlAsync(responseString, unityVersion);
143 | }
144 | catch (Exception e)
145 | {
146 | Console.WriteLine($"Error fetching download URL: {e.Message}");
147 | return null;
148 | }
149 | }
150 |
151 | static string ParseHashCodeFromURL(string url)
152 | {
153 | // https://beta.unity3d.com/download/330fbefc18b7/download.html#6000.1.0a8 > 330fbefc18b7
154 |
155 | int hashStart = url.IndexOf("download/") + 9;
156 | int hashEnd = url.IndexOf("/download.html", hashStart);
157 | return url.Substring(hashStart, hashEnd - hashStart);
158 | }
159 |
160 | private static async Task ExtractDownloadUrlAsync(string json, string unityVersion)
161 | {
162 | //Console.WriteLine("json: " + json + " vers: " + unityVersion);
163 |
164 | if (json.Contains("\"results\":[]"))
165 | {
166 | Console.WriteLine("No results found from releases API, checking unofficial list (if enabled)");
167 |
168 | if (unofficialReleaseURLs.ContainsKey(unityVersion))
169 | {
170 | Console.WriteLine("Unofficial release found in the list.");
171 |
172 | string unityHash = ParseHashCodeFromURL(unofficialReleaseURLs[unityVersion]);
173 | // Console.WriteLine(unityHash);
174 | string downloadURL = Tools.ParseDownloadURLFromWebpage(unityVersion, unityHash, false, true);
175 | // Console.WriteLine("direct download url: "+downloadURL);
176 | return downloadURL;
177 | }
178 |
179 | return null;
180 | }
181 |
182 | int resultsIndex = json.IndexOf("\"results\":");
183 | if (resultsIndex == -1) return null;
184 |
185 | int downloadsIndex = json.IndexOf("\"downloads\":", resultsIndex);
186 | if (downloadsIndex == -1) return null;
187 |
188 | int urlIndex = json.IndexOf("\"url\":", downloadsIndex);
189 | if (urlIndex == -1) return null;
190 |
191 | int urlStart = json.IndexOf('"', urlIndex + 6) + 1;
192 | int urlEnd = json.IndexOf('"', urlStart);
193 | string downloadUrl = json.Substring(urlStart, urlEnd - urlStart);
194 |
195 | int revisionIndex = json.IndexOf("\"shortRevision\":", resultsIndex);
196 | string shortRevision = null;
197 | if (revisionIndex != -1)
198 | {
199 | int revisionStart = json.IndexOf('"', revisionIndex + 16) + 1;
200 | int revisionEnd = json.IndexOf('"', revisionStart);
201 | shortRevision = json.Substring(revisionStart, revisionEnd - revisionStart);
202 | }
203 |
204 | if (!string.IsNullOrEmpty(downloadUrl) && !string.IsNullOrEmpty(shortRevision))
205 | {
206 | int revisionPosition = downloadUrl.LastIndexOf(shortRevision, StringComparison.Ordinal) + shortRevision.Length + 1;
207 | string assistantUrl = downloadUrl.Substring(0, revisionPosition) + $"UnityDownloadAssistant-{unityVersion}.exe";
208 |
209 | if (await CheckAssistantUrl(assistantUrl))
210 | {
211 | //Console.WriteLine("ExtractDownloadUrlAsync: Assistant download URL found.");
212 | return assistantUrl;
213 | }
214 | else
215 | {
216 | Console.WriteLine("Assistant download URL not found, returning original download URL.");
217 | return downloadUrl;
218 | }
219 | }
220 |
221 | Console.WriteLine("Returning original download URL.");
222 | return downloadUrl;
223 | }
224 |
225 | private static async Task CheckAssistantUrl(string assistantUrl)
226 | {
227 | try
228 | {
229 | using (HttpResponseMessage response = await Client.GetAsync(assistantUrl))
230 | {
231 | return response.IsSuccessStatusCode;
232 | }
233 | }
234 | catch (Exception ex)
235 | {
236 | Console.WriteLine($"Request failed: {ex.Message}");
237 | return false;
238 | }
239 | }
240 |
241 | private static async Task> FetchNewVersions(List cachedVersions)
242 | {
243 | var newVersions = new List();
244 | var cachedVersionSet = new HashSet(cachedVersions.Select(v => v.Version));
245 | int offset = 0;
246 | int total = int.MaxValue;
247 | bool foundNewVersionInBatch;
248 |
249 | while (offset < total)
250 | {
251 | var batchUpdates = await FetchBatch(offset);
252 | if (batchUpdates == null || batchUpdates.Count == 0) break;
253 |
254 | foundNewVersionInBatch = false;
255 |
256 | foreach (var version in batchUpdates)
257 | {
258 | if (!cachedVersionSet.Contains(version.Version))
259 | {
260 | newVersions.Add(version);
261 | foundNewVersionInBatch = true;
262 | }
263 | }
264 |
265 | if (!foundNewVersionInBatch)
266 | {
267 | // Exit if no new versions are found in the current batch
268 | break;
269 | }
270 |
271 | offset += batchUpdates.Count;
272 |
273 | // Apply delay if reaching batch limit
274 | if (offset % (BatchSize * RequestsPerBatch) == 0)
275 | {
276 | await Task.Delay(DelayBetweenBatches);
277 | }
278 | }
279 |
280 | return newVersions;
281 | }
282 |
283 |
284 |
285 | private static async Task> FetchBatch(int offset)
286 | {
287 | string url = $"{BaseApiUrl}?limit={BatchSize}&offset={offset}&architecture=X86_64&platform=WINDOWS";
288 |
289 | try
290 | {
291 | string response = await Client.GetStringAsync(url);
292 | return ParseUnityVersions(response);
293 | }
294 | catch (Exception e)
295 | {
296 | Console.WriteLine($"Error fetching batch: {e.Message}");
297 | return null;
298 | }
299 | }
300 |
301 | private static List ParseUnityVersions(string json)
302 | {
303 | var versions = new List();
304 | int resultsIndex = json.IndexOf("\"results\":");
305 | if (resultsIndex == -1) return versions;
306 |
307 | string[] items = json.Substring(resultsIndex).Split(new[] { "{" }, StringSplitOptions.RemoveEmptyEntries);
308 |
309 | foreach (string item in items)
310 | {
311 | if (item.Contains("\"version\""))
312 | {
313 | var version = new UnityVersion
314 | {
315 | Version = GetStringValue(item, "version"),
316 | ReleaseDate = DateTime.TryParse(GetStringValue(item, "releaseDate"), out var date) ? date : default,
317 | Stream = Enum.TryParse(GetStringValue(item, "stream"), true, out var stream) ? stream : UnityVersionStream.Tech
318 | };
319 | //Console.WriteLine(version.Version);
320 | versions.Add(version);
321 | }
322 | }
323 |
324 | return versions;
325 | }
326 |
327 | private static List ParseCachedUnityVersions(string json)
328 | {
329 | var versions = new List();
330 |
331 | // Remove square brackets at the beginning and end of the array
332 | json = json.Trim(new[] { '[', ']' });
333 |
334 | // Split each item based on the closing bracket and opening bracket of consecutive objects
335 | string[] items = json.Split(new[] { "},{" }, StringSplitOptions.RemoveEmptyEntries);
336 |
337 | foreach (string item in items)
338 | {
339 | // Ensure each item is properly enclosed in braces to handle edge cases
340 | string cleanItem = "{" + item.Trim(new[] { '{', '}' }) + "}";
341 |
342 | // Parse each UnityVersion object
343 | if (cleanItem.Contains("\"version\""))
344 | {
345 | var version = new UnityVersion
346 | {
347 | Version = GetStringValue(cleanItem, "version"),
348 | ReleaseDate = DateTime.TryParse(GetStringValue(cleanItem, "releaseDate"), out var date) ? date : default,
349 | Stream = Enum.TryParse(GetStringValue(cleanItem, "stream"), true, out var stream) ? stream : UnityVersionStream.Tech
350 | };
351 | versions.Add(version);
352 | }
353 | }
354 |
355 | return versions;
356 | }
357 |
358 | //private static string GetStringValue(string source, string propertyName)
359 | //{
360 | // int propertyIndex = source.IndexOf($"\"{propertyName}\":");
361 | // if (propertyIndex == -1) return null;
362 |
363 | // int valueStart = source.IndexOf('"', propertyIndex + propertyName.Length + 2) + 1;
364 | // int valueEnd = source.IndexOf('"', valueStart);
365 | // return source.Substring(valueStart, valueEnd - valueStart);
366 | //}
367 |
368 |
369 | private static string GetStringValue(string source, string propertyName)
370 | {
371 | int propertyIndex = source.IndexOf($"\"{propertyName}\":");
372 | if (propertyIndex == -1) return null;
373 |
374 | int valueStart = source.IndexOf('"', propertyIndex + propertyName.Length + 2) + 1;
375 | int valueEnd = source.IndexOf('"', valueStart);
376 | return source.Substring(valueStart, valueEnd - valueStart);
377 | }
378 |
379 | private static List LoadCachedVersions()
380 | {
381 | string configFilePath = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
382 | string configDirectory = Path.GetDirectoryName(configFilePath);
383 | if (configDirectory == null) return new List();
384 |
385 | string cacheFilePath = Path.Combine(configDirectory, CacheFileName);
386 | if (!File.Exists(cacheFilePath)) return new List();
387 |
388 | string json = File.ReadAllText(cacheFilePath);
389 | return ParseCachedUnityVersions(json);
390 | }
391 |
392 | private static void SaveCachedVersions(List versions)
393 | {
394 | string json = "[";
395 | foreach (var version in versions)
396 | {
397 | json += $"{{\"version\":\"{version.Version}\",\"releaseDate\":\"{version.ReleaseDate:O}\",\"stream\":\"{version.Stream}\"}},";
398 | }
399 | json = json.TrimEnd(',') + "]";
400 |
401 | string configFilePath = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
402 | string configDirectory = Path.GetDirectoryName(configFilePath);
403 | if (configDirectory == null) return;
404 |
405 | string cacheFilePath = Path.Combine(configDirectory, CacheFileName);
406 | //Console.WriteLine("Saving cachedrelease: " + cacheFilePath);
407 | File.WriteAllText(cacheFilePath, json);
408 | }
409 | }
410 | }
411 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Helpers/ObservableDictionary.cs:
--------------------------------------------------------------------------------
1 | // Licensed by Daniel Cazzulino under the MIT License : https://gist.github.com/kzu/cfe3cb6e4fe3efea6d24
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Collections.Specialized;
5 | using System.ComponentModel;
6 | using System.Diagnostics;
7 |
8 | namespace UnityLauncherPro.Helpers
9 | {
10 | ///
11 | /// Provides a dictionary for use with data binding.
12 | ///
13 | /// Specifies the type of the keys in this collection.
14 | /// Specifies the type of the values in this collection.
15 | [DebuggerDisplay("Count={Count}")]
16 | public class ObservableDictionary :
17 | ICollection>, IDictionary,
18 | INotifyCollectionChanged, INotifyPropertyChanged
19 | {
20 | readonly IDictionary dictionary;
21 |
22 | /// Event raised when the collection changes.
23 | public event NotifyCollectionChangedEventHandler CollectionChanged = (sender, args) => { };
24 |
25 | /// Event raised when a property on the collection changes.
26 | public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { };
27 |
28 | ///
29 | /// Initializes an instance of the class.
30 | ///
31 | public ObservableDictionary()
32 | : this(new Dictionary())
33 | {
34 | }
35 |
36 | ///
37 | /// Initializes an instance of the class using another dictionary as
38 | /// the key/value store.
39 | ///
40 | public ObservableDictionary(IDictionary dictionary)
41 | {
42 | this.dictionary = dictionary;
43 | }
44 |
45 | void AddWithNotification(KeyValuePair item)
46 | {
47 | AddWithNotification(item.Key, item.Value);
48 | }
49 |
50 | void AddWithNotification(TKey key, TValue value)
51 | {
52 | dictionary.Add(key, value);
53 |
54 | CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,
55 | new KeyValuePair(key, value)));
56 | PropertyChanged(this, new PropertyChangedEventArgs("Count"));
57 | PropertyChanged(this, new PropertyChangedEventArgs("Keys"));
58 | PropertyChanged(this, new PropertyChangedEventArgs("Values"));
59 | }
60 |
61 | bool RemoveWithNotification(TKey key)
62 | {
63 | TValue value;
64 | if (dictionary.TryGetValue(key, out value) && dictionary.Remove(key))
65 | {
66 | CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,
67 | new KeyValuePair(key, value)));
68 | PropertyChanged(this, new PropertyChangedEventArgs("Count"));
69 | PropertyChanged(this, new PropertyChangedEventArgs("Keys"));
70 | PropertyChanged(this, new PropertyChangedEventArgs("Values"));
71 |
72 | return true;
73 | }
74 |
75 | return false;
76 | }
77 |
78 | void UpdateWithNotification(TKey key, TValue value)
79 | {
80 | TValue existing;
81 | if (dictionary.TryGetValue(key, out existing))
82 | {
83 | dictionary[key] = value;
84 |
85 | CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,
86 | new KeyValuePair(key, value),
87 | new KeyValuePair(key, existing)));
88 | PropertyChanged(this, new PropertyChangedEventArgs("Values"));
89 | }
90 | else
91 | {
92 | AddWithNotification(key, value);
93 | }
94 | }
95 |
96 | ///
97 | /// Allows derived classes to raise custom property changed events.
98 | ///
99 | protected void RaisePropertyChanged(PropertyChangedEventArgs args)
100 | {
101 | PropertyChanged(this, args);
102 | }
103 |
104 | #region IDictionary Members
105 |
106 | ///
107 | /// Adds an element with the provided key and value to the .
108 | ///
109 | /// The object to use as the key of the element to add.
110 | /// The object to use as the value of the element to add.
111 | public void Add(TKey key, TValue value)
112 | {
113 | AddWithNotification(key, value);
114 | }
115 |
116 | ///
117 | /// Determines whether the contains an element with the specified key.
118 | ///
119 | /// The key to locate in the .
120 | ///
121 | /// true if the contains an element with the key; otherwise, false.
122 | ///
123 | public bool ContainsKey(TKey key)
124 | {
125 | return dictionary.ContainsKey(key);
126 | }
127 |
128 | ///
129 | /// Gets an containing the keys of the .
130 | ///
131 | /// An containing the keys of the object that implements .
132 | public ICollection Keys
133 | {
134 | get { return dictionary.Keys; }
135 | }
136 |
137 | ///
138 | /// Removes the element with the specified key from the .
139 | ///
140 | /// The key of the element to remove.
141 | ///
142 | /// true if the element is successfully removed; otherwise, false. This method also returns false if was not found in the original .
143 | ///
144 | public bool Remove(TKey key)
145 | {
146 | return RemoveWithNotification(key);
147 | }
148 |
149 | ///
150 | /// Gets the value associated with the specified key.
151 | ///
152 | /// The key whose value to get.
153 | /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized.
154 | ///
155 | /// true if the object that implements contains an element with the specified key; otherwise, false.
156 | ///
157 | public bool TryGetValue(TKey key, out TValue value)
158 | {
159 | return dictionary.TryGetValue(key, out value);
160 | }
161 |
162 | ///
163 | /// Gets an containing the values in the .
164 | ///
165 | /// An containing the values in the object that implements .
166 | public ICollection Values
167 | {
168 | get { return dictionary.Values; }
169 | }
170 |
171 | ///
172 | /// Gets or sets the element with the specified key.
173 | ///
174 | /// The key.
175 | ///
176 | public TValue this[TKey key]
177 | {
178 | get { return dictionary[key]; }
179 | set { UpdateWithNotification(key, value); }
180 | }
181 |
182 | #endregion
183 |
184 | #region ICollection> Members
185 |
186 | void ICollection>.Add(KeyValuePair item)
187 | {
188 | AddWithNotification(item);
189 | }
190 |
191 | void ICollection>.Clear()
192 | {
193 | ((ICollection>)dictionary).Clear();
194 |
195 | CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
196 | PropertyChanged(this, new PropertyChangedEventArgs("Count"));
197 | PropertyChanged(this, new PropertyChangedEventArgs("Keys"));
198 | PropertyChanged(this, new PropertyChangedEventArgs("Values"));
199 | }
200 |
201 | bool ICollection>.Contains(KeyValuePair item)
202 | {
203 | return ((ICollection>)dictionary).Contains(item);
204 | }
205 |
206 | void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex)
207 | {
208 | ((ICollection>)dictionary).CopyTo(array, arrayIndex);
209 | }
210 |
211 | int ICollection>.Count
212 | {
213 | get { return ((ICollection>)dictionary).Count; }
214 | }
215 |
216 | bool ICollection>.IsReadOnly
217 | {
218 | get { return ((ICollection>)dictionary).IsReadOnly; }
219 | }
220 |
221 | bool ICollection>.Remove(KeyValuePair item)
222 | {
223 | return RemoveWithNotification(item.Key);
224 | }
225 |
226 | #endregion
227 |
228 | #region IEnumerable> Members
229 |
230 | IEnumerator> IEnumerable>.GetEnumerator()
231 | {
232 | return ((ICollection>)dictionary).GetEnumerator();
233 | }
234 |
235 | IEnumerator IEnumerable.GetEnumerator()
236 | {
237 | return ((ICollection>)dictionary).GetEnumerator();
238 | }
239 |
240 | #endregion
241 |
242 | // https://github.com/unitycoder/UnityLauncherPro/issues/16 added Clear https://stackoverflow.com/q/5663395/5452781
243 | public void Clear()
244 | {
245 | dictionary.Clear();
246 | CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
247 | PropertyChanged(this, new PropertyChangedEventArgs("Count"));
248 | PropertyChanged(this, new PropertyChangedEventArgs("Keys"));
249 | PropertyChanged(this, new PropertyChangedEventArgs("Values"));
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Helpers/ProcessHandler.cs:
--------------------------------------------------------------------------------
1 | // keeps reference of launched unity processes, so that can close them even if project list is refreshed
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.Runtime.InteropServices;
7 | using System.Windows;
8 | using System.Windows.Threading;
9 |
10 | namespace UnityLauncherPro.Helpers
11 | {
12 | public static class ProcessHandler
13 | {
14 | static Dictionary processes = new Dictionary();
15 |
16 | public static void Add(Project proj, Process proc)
17 | {
18 | if (proc == null) return;
19 |
20 | var key = proj.Path;
21 | if (processes.ContainsKey(key))
22 | {
23 | // already in the list, maybe trying to launch same project twice? only overwrite if previous process has closed
24 | if (processes[key] == null) processes[key] = proc;
25 | }
26 | else
27 | {
28 | processes.Add(key, proc);
29 | }
30 |
31 | // subscribe to process exit here, so that can update proj details row (if it was changed in Unity)
32 | proc.Exited += (object o, EventArgs ea) =>
33 | {
34 | // call method in mainwindow, to easy access for sourcedata and grid
35 | Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, (Action)delegate ()
36 | {
37 | MainWindow wnd = (MainWindow)Application.Current.MainWindow;
38 | wnd.ProcessExitedCallBack(proj);
39 | });
40 |
41 | // remove closed process item
42 | Remove(key);
43 | };
44 | }
45 |
46 | public static Process Get(string key)
47 | {
48 | if (processes.ContainsKey(key)) return processes[key];
49 | return null;
50 | }
51 |
52 | public static bool IsRunning(string key)
53 | {
54 | return processes.ContainsKey(key) && (processes[key] != null);
55 | }
56 |
57 | public static void Remove(string key)
58 | {
59 | if (processes.ContainsKey(key)) processes.Remove(key);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Images/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unitycoder/UnityLauncherPro/0febfad4b7e47901fc541a998ce2596fab9ef310/UnityLauncherPro/Images/icon.ico
--------------------------------------------------------------------------------
/UnityLauncherPro/Images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unitycoder/UnityLauncherPro/0febfad4b7e47901fc541a998ce2596fab9ef310/UnityLauncherPro/Images/icon.png
--------------------------------------------------------------------------------
/UnityLauncherPro/Libraries/ExtractTarGz.cs:
--------------------------------------------------------------------------------
1 | // source https://gist.github.com/Su-s/438be493ae692318c73e30367cbc5c2a
2 | // updated source https://gist.github.com/Matheos96/da8990030dfe3e27b0a48722042d9c0b
3 |
4 | using System;
5 | using System.IO;
6 | using System.IO.Compression;
7 | using System.Text;
8 |
9 | namespace TarLib
10 | {
11 | public class Tar
12 | {
13 | ///
14 | /// Extracts a .tar.gz archive to the specified directory.
15 | ///
16 | /// The .tar.gz to decompress and extract.
17 | /// Output directory to write the files.
18 | public static void ExtractTarGz(string filename, string outputDir)
19 | {
20 | using (var stream = File.OpenRead(filename))
21 | {
22 | ExtractTarGz(stream, outputDir);
23 | }
24 | }
25 |
26 | ///
27 | /// Extracts a .tar.gz archive stream to the specified directory.
28 | ///
29 | /// The .tar.gz to decompress and extract.
30 | /// Output directory to write the files.
31 | public static void ExtractTarGz(Stream stream, string outputDir)
32 | {
33 | int read;
34 | const int chunk = 4096;
35 | var buffer = new byte[chunk];
36 |
37 | // A GZipStream is not seekable, so copy it first to a MemoryStream
38 | using (var gzipStream = new GZipStream(stream, CompressionMode.Decompress))
39 | {
40 | using (var memStream = new MemoryStream())
41 | {
42 | //For .NET 6+
43 | while ((read = gzipStream.Read(buffer, 0, buffer.Length)) > 0)
44 | {
45 | memStream.Write(buffer, 0, read);
46 | }
47 | memStream.Seek(0, SeekOrigin.Begin);
48 |
49 | //ExtractTar(gzip, outputDir);
50 | ExtractTar(memStream, outputDir);
51 | }
52 | }
53 | }
54 |
55 | ///
56 | /// Extractes a tar archive to the specified directory.
57 | ///
58 | /// The .tar to extract.
59 | /// Output directory to write the files.
60 | public static void ExtractTar(string filename, string outputDir)
61 | {
62 | using (var stream = File.OpenRead(filename))
63 | {
64 | ExtractTar(stream, outputDir);
65 | }
66 | }
67 |
68 | ///
69 | /// Extractes a tar archive to the specified directory.
70 | ///
71 | /// The .tar to extract.
72 | /// Output directory to write the files.
73 | public static void ExtractTar(Stream stream, string outputDir)
74 | {
75 | var buffer = new byte[100];
76 | var longFileName = string.Empty;
77 | while (true)
78 | {
79 | stream.Read(buffer, 0, 100);
80 | string name = string.IsNullOrEmpty(longFileName) ? Encoding.ASCII.GetString(buffer).Trim('\0') : longFileName; //Use longFileName if we have one read
81 |
82 | if (String.IsNullOrWhiteSpace(name)) break;
83 | stream.Seek(24, SeekOrigin.Current);
84 | stream.Read(buffer, 0, 12);
85 | var size = Convert.ToInt64(Encoding.UTF8.GetString(buffer, 0, 12).Trim('\0').Trim(), 8);
86 | stream.Seek(20, SeekOrigin.Current); //Move head to typeTag byte
87 | var typeTag = stream.ReadByte();
88 | stream.Seek(355L, SeekOrigin.Current); //Move head to beginning of data (byte 512)
89 |
90 | if (typeTag == 'L')
91 | {
92 | //If Type Tag is 'L' we have a filename that is longer than the 100 bytes reserved for it in the header.
93 | //We read it here and save it temporarily as it will be the file name of the next block where the actual data is
94 | var buf = new byte[size];
95 | stream.Read(buf, 0, buf.Length);
96 | longFileName = Encoding.ASCII.GetString(buf).Trim('\0');
97 | }
98 | else
99 | {
100 | longFileName = string.Empty; //Reset longFileName if current entry is not indicating one
101 |
102 | var output = Path.Combine(outputDir, name);
103 |
104 | // only include these folders
105 | var include = (output.IndexOf("package/ProjectData~/Assets/") > -1);
106 | include |= (output.IndexOf("package/ProjectData~/ProjectSettings/") > -1);
107 | include |= (output.IndexOf("package/ProjectData~/Packages/") > -1);
108 |
109 | // rename output path from "package/ProjectData~/Assets/" into "Assets/"
110 | output = output.Replace("package/ProjectData~/", "");
111 |
112 | if (include == true && !Directory.Exists(Path.GetDirectoryName(output))) Directory.CreateDirectory(Path.GetDirectoryName(output));
113 |
114 | // not folder
115 | //if (name.Equals("./", StringComparison.InvariantCulture) == false)
116 | if (name.EndsWith("/") == false) //Directories are zero size and don't need anything written
117 | {
118 | if (include == true)
119 | {
120 | //Console.WriteLine("output=" + output);
121 | using (var str = File.Open(output, FileMode.OpenOrCreate, FileAccess.ReadWrite))
122 | {
123 | var buf = new byte[size];
124 | stream.Read(buf, 0, buf.Length);
125 | // take only data from this folder
126 | str.Write(buf, 0, buf.Length);
127 | }
128 | }
129 | else
130 | {
131 | var buf = new byte[size];
132 | stream.Read(buf, 0, buf.Length);
133 | }
134 | }
135 | }
136 |
137 | //Move head to next 512 byte block
138 | var pos = stream.Position;
139 | var offset = 512 - (pos % 512);
140 | if (offset == 512) offset = 0;
141 |
142 | stream.Seek(offset, SeekOrigin.Current);
143 | }
144 | }
145 | } // class Tar
146 | } // namespace TarLib
147 |
148 |
149 | /*
150 | This software is available under 2 licenses-- choose whichever you prefer.
151 | ------------------------------------------------------------------------------
152 | ALTERNATIVE A - MIT License
153 | Copyright (c) 2017 Sean Barrett
154 | Permission is hereby granted, free of charge, to any person obtaining a copy of
155 | this software and associated documentation files (the "Software"), to deal in
156 | the Software without restriction, including without limitation the rights to
157 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
158 | of the Software, and to permit persons to whom the Software is furnished to do
159 | so, subject to the following conditions:
160 | The above copyright notice and this permission notice shall be included in all
161 | copies or substantial portions of the Software.
162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
168 | SOFTWARE.
169 | ------------------------------------------------------------------------------
170 | ALTERNATIVE B - Public Domain (www.unlicense.org)
171 | This is free and unencumbered software released into the public domain.
172 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
173 | software, either in source code form or as a compiled binary, for any purpose,
174 | commercial or non-commercial, and by any means.
175 | In jurisdictions that recognize copyright laws, the author or authors of this
176 | software dedicate any and all copyright interest in the software to the public
177 | domain.We make this dedication for the benefit of the public at large and to
178 | the detriment of our heirs and successors. We intend this dedication to be an
179 | overt act of relinquishment in perpetuity of all present and future rights to
180 | this software under copyright law.
181 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
182 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
183 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
184 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
185 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
186 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
187 | */
188 |
--------------------------------------------------------------------------------
/UnityLauncherPro/NewProject.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/UnityLauncherPro/NewProject.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Windows;
5 | using System.Windows.Controls;
6 | using System.Windows.Input;
7 | using System.Windows.Media;
8 |
9 | namespace UnityLauncherPro
10 | {
11 | public partial class NewProject : Window
12 | {
13 | public static string newProjectName = null;
14 | public static string newVersion = null;
15 | public static string newName = null;
16 | public static string templateZipPath = null;
17 | public static string selectedPlatform = null;
18 | public static bool forceDX11 = false;
19 | public static string[] platformsForThisUnity = null;
20 |
21 | bool isInitializing = true; // to keep OnChangeEvent from firing too early
22 | int previousSelectedTemplateIndex = -1;
23 | int previousSelectedModuleIndex = -1;
24 |
25 | static string targetFolder = null;
26 |
27 | public NewProject(string unityVersion, string suggestedName, string targetFolder, bool nameIsLocked = false)
28 | {
29 | isInitializing = true;
30 | InitializeComponent();
31 |
32 | NewProject.targetFolder = targetFolder;
33 |
34 | // get version
35 | newVersion = unityVersion;
36 | newName = suggestedName;
37 |
38 | txtNewProjectName.IsEnabled = !nameIsLocked;
39 |
40 | txtNewProjectName.Text = newName;
41 | lblNewProjectFolder.Content = targetFolder;
42 |
43 | // fill available versions
44 | if (gridAvailableVersions.ItemsSource == null)
45 | {
46 | gridAvailableVersions.ItemsSource = MainWindow.unityInstallationsSource;
47 | }
48 |
49 | // we have that version installed
50 | if (MainWindow.unityInstalledVersions.ContainsKey(unityVersion) == true)
51 | {
52 | // find this unity version, TODO theres probably easier way than looping all
53 | for (int i = 0; i < MainWindow.unityInstallationsSource.Count; i++)
54 | {
55 | if (MainWindow.unityInstallationsSource[i].Version == newVersion)
56 | {
57 | gridAvailableVersions.SelectedIndex = i;
58 | gridAvailableVersions.ScrollIntoView(gridAvailableVersions.SelectedItem);
59 | break;
60 | }
61 | }
62 |
63 | UpdateTemplatesDropDown((gridAvailableVersions.SelectedItem as UnityInstallation).Path);
64 | UpdateModulesDropdown(newVersion);
65 | }
66 | else // we dont have requested unity version, select first item then
67 | {
68 | var path = MainWindow.unityInstallationsSource[0].Path;
69 | gridAvailableVersions.SelectedIndex = 0;
70 | gridAvailableVersions.ScrollIntoView(gridAvailableVersions.Items[0]);
71 | UpdateTemplatesDropDown(path);
72 | }
73 |
74 | // select projectname text so can overwrite if needed
75 | txtNewProjectName.Focus();
76 | txtNewProjectName.SelectAll();
77 | newProjectName = txtNewProjectName.Text;
78 |
79 | isInitializing = false;
80 | } // NewProject
81 |
82 | void UpdateTemplatesDropDown(string unityPath)
83 | {
84 | // scan available templates, TODO could cache this at least per session?
85 | cmbNewProjectTemplate.ItemsSource = Tools.ScanTemplates(unityPath);
86 | cmbNewProjectTemplate.SelectedIndex = 0;
87 | lblTemplateTitleAndCount.Content = "Templates: (" + (cmbNewProjectTemplate.Items.Count - 1) + ")";
88 | }
89 |
90 |
91 | void UpdateModulesDropdown(string version)
92 | {
93 | // get modules and stick into combobox, NOTE we already have this info from GetProjects.Scan, so could access it
94 | platformsForThisUnity = Tools.GetPlatformsForUnityVersion(version);
95 | cmbNewProjectPlatform.ItemsSource = platformsForThisUnity;
96 |
97 | var lastUsedPlatform = Properties.Settings.Default.newProjectPlatform;
98 |
99 | for (int i = 0; i < platformsForThisUnity.Length; i++)
100 | {
101 | // set default platform (win64) if never used this setting before
102 | if ((string.IsNullOrEmpty(lastUsedPlatform) && platformsForThisUnity[i].ToLower() == "win64") || platformsForThisUnity[i] == lastUsedPlatform)
103 | {
104 | cmbNewProjectPlatform.SelectedIndex = i;
105 | break;
106 | }
107 | }
108 |
109 | // if nothing found, use win64
110 | if (cmbNewProjectPlatform.SelectedIndex == -1)
111 | {
112 | //cmbNewProjectPlatform.SelectedIndex = cmbNewProjectPlatform.Items.Count > 1 ? 1 : 0;
113 | for (int i = 0; i < platformsForThisUnity.Length; i++)
114 | {
115 | if (platformsForThisUnity[i].ToLower() == "win64")
116 | {
117 | cmbNewProjectPlatform.SelectedIndex = i;
118 | break;
119 | }
120 | }
121 |
122 | // if still nothing, use first
123 | if (cmbNewProjectPlatform.SelectedIndex == -1) cmbNewProjectPlatform.SelectedIndex = 0;
124 | //lblTemplateTitleAndCount.Content = "Templates: (" + (cmbNewProjectTemplate.Items.Count - 1) + ")";
125 | }
126 | }
127 |
128 | private void BtnCreateNewProject_Click(object sender, RoutedEventArgs e)
129 | {
130 | // check if projectname already exists (only if should be automatically created name)
131 | var targetPath = Path.Combine(targetFolder, txtNewProjectName.Text);
132 | if (txtNewProjectName.IsEnabled == true && Directory.Exists(targetPath) == true)
133 | {
134 | Tools.SetStatus("Project already exists: " + txtNewProjectName.Text);
135 | return;
136 | }
137 |
138 | templateZipPath = ((KeyValuePair)cmbNewProjectTemplate.SelectedValue).Value;
139 | selectedPlatform = cmbNewProjectPlatform.SelectedValue.ToString();
140 | UpdateSelectedVersion();
141 |
142 | // save last used value for platform
143 | Properties.Settings.Default.newProjectPlatform = cmbNewProjectPlatform.SelectedValue.ToString();
144 | Properties.Settings.Default.Save();
145 |
146 | DialogResult = true;
147 | }
148 |
149 | private void BtnCancelNewProject_Click(object sender, RoutedEventArgs e)
150 | {
151 | DialogResult = false;
152 | }
153 |
154 |
155 | private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
156 | {
157 | switch (e.Key)
158 | {
159 | case Key.Tab:
160 | // manually tab into next component (automatic tabstops not really working here)
161 | TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next);
162 | UIElement keyboardFocus = Keyboard.FocusedElement as UIElement;
163 | if (keyboardFocus != null)
164 | {
165 | keyboardFocus.MoveFocus(tRequest);
166 | }
167 | break;
168 | case Key.F2: // select project name field
169 | txtNewProjectName.Focus();
170 | txtNewProjectName.SelectAll();
171 | break;
172 | case Key.F3: // next platform
173 | cmbNewProjectPlatform.SelectedIndex = ++cmbNewProjectPlatform.SelectedIndex % cmbNewProjectPlatform.Items.Count;
174 | break;
175 | case Key.F4: // next template
176 | case Key.Oem5: // select next template §-key
177 | cmbNewProjectTemplate.SelectedIndex = ++cmbNewProjectTemplate.SelectedIndex % cmbNewProjectTemplate.Items.Count;
178 | e.Handled = true; // override writing to textbox
179 | break;
180 | case Key.Enter: // enter, create proj
181 | BtnCreateNewProject_Click(null, null);
182 | e.Handled = true;
183 | break;
184 | case Key.Escape: // esc cancel
185 | // if pressed esc while combobox is open, close that one instead of closing window
186 | if (cmbNewProjectTemplate.IsDropDownOpen)
187 | {
188 | cmbNewProjectTemplate.IsDropDownOpen = false;
189 | if (previousSelectedTemplateIndex > -1) cmbNewProjectTemplate.SelectedIndex = previousSelectedTemplateIndex;
190 | return;
191 | }
192 |
193 | if (cmbNewProjectPlatform.IsDropDownOpen)
194 | {
195 | cmbNewProjectPlatform.IsDropDownOpen = false;
196 | if (previousSelectedModuleIndex > -1) cmbNewProjectPlatform.SelectedIndex = previousSelectedModuleIndex;
197 | return;
198 | }
199 |
200 | DialogResult = false;
201 | e.Handled = true;
202 | break;
203 | default:
204 | break;
205 | }
206 | }
207 |
208 | void UpdateSelectedVersion()
209 | {
210 | var k = gridAvailableVersions.SelectedItem as UnityInstallation;
211 | if (k != null && k.Version != newVersion)
212 | {
213 | newVersion = k.Version;
214 | }
215 | }
216 |
217 | private void TxtNewProjectName_TextChanged(object sender, TextChangedEventArgs e)
218 | {
219 | if (isInitializing == true) return;
220 |
221 | // warning yellow if contains space at start or end
222 | if (txtNewProjectName.Text.StartsWith(" ") || txtNewProjectName.Text.EndsWith(" "))
223 | {
224 | // NOTE txtbox outline didnt work
225 | txtNewProjectName.Background = Brushes.Yellow;
226 | txtNewProjectStatus.Text = "Warning: Project name starts or ends with SPACE character";
227 | txtNewProjectStatus.Foreground = Brushes.Orange;
228 | }
229 | else
230 | {
231 | // NOTE this element is not using themes yet, so can set white
232 | txtNewProjectName.Background = Brushes.White;
233 | txtNewProjectStatus.Foreground = Brushes.White;
234 | txtNewProjectStatus.Text = "";
235 | }
236 |
237 | // validate new projectname that it doesnt exists already
238 | var targetPath = Path.Combine(targetFolder, txtNewProjectName.Text);
239 | if (Directory.Exists(targetPath) == true)
240 | {
241 | System.Console.WriteLine("Project already exists");
242 | txtNewProjectName.BorderBrush = Brushes.Red; // not visible if focused
243 | txtNewProjectName.ToolTip = "Project folder already exists";
244 | btnCreateNewProject.IsEnabled = false;
245 | }
246 | else
247 | {
248 | txtNewProjectName.BorderBrush = null;
249 | btnCreateNewProject.IsEnabled = true;
250 | txtNewProjectName.ToolTip = "";
251 | }
252 |
253 | //System.Console.WriteLine("newProjectName: " + txtNewProjectName.Text);
254 |
255 | newProjectName = txtNewProjectName.Text;
256 | }
257 |
258 | private void TxtNewProjectName_PreviewKeyDown(object sender, KeyEventArgs e)
259 | {
260 | switch (e.Key)
261 | {
262 | case Key.PageUp:
263 | case Key.PageDown:
264 | case Key.Up:
265 | case Key.Down:
266 | Tools.SetFocusToGrid(gridAvailableVersions);
267 | break;
268 | default:
269 | break;
270 | }
271 | }
272 |
273 | void GenerateNewName()
274 | {
275 | var newProj = Tools.GetSuggestedProjectName(newVersion, lblNewProjectFolder.Content.ToString());
276 | txtNewProjectName.Text = newProj;
277 | }
278 |
279 | // FIXME this gets called when list is updated?
280 | private void GridAvailableVersions_SelectionChanged(object sender, SelectionChangedEventArgs e)
281 | {
282 | if (gridAvailableVersions.SelectedItem == null || isInitializing == true) return;
283 | // new row selected, generate new project name for this version
284 | var k = gridAvailableVersions.SelectedItem as UnityInstallation;
285 | newVersion = k.Version;
286 | // no new name, if field is locked (because its folder name then)
287 | if (txtNewProjectName.IsEnabled == true) GenerateNewName();
288 |
289 | // update templates list for selected unity version
290 | UpdateTemplatesDropDown(k.Path);
291 | UpdateModulesDropdown(k.Version);
292 |
293 | // hide forceDX11 checkbox if version is below 6000
294 | bool is6000 = k.Version.Contains("6000");
295 | chkForceDX11.Visibility = is6000 ? Visibility.Visible : Visibility.Collapsed;
296 | }
297 |
298 | private void GridAvailableVersions_Loaded(object sender, RoutedEventArgs e)
299 | {
300 | // set initial default row color
301 | DataGridRow row = (DataGridRow)gridAvailableVersions.ItemContainerGenerator.ContainerFromIndex(gridAvailableVersions.SelectedIndex);
302 | // if no unitys available
303 | if (row == null) return;
304 | //row.Background = Brushes.Green;
305 | row.Foreground = Brushes.White;
306 | row.FontWeight = FontWeights.Bold;
307 | }
308 |
309 | private void CmbNewProjectTemplate_DropDownOpened(object sender, System.EventArgs e)
310 | {
311 | // on open, take current selection, so can undo later
312 | previousSelectedTemplateIndex = cmbNewProjectTemplate.SelectedIndex;
313 | }
314 |
315 | private void CmbNewProjectPlatform_DropDownOpened(object sender, System.EventArgs e)
316 | {
317 | previousSelectedModuleIndex = cmbNewProjectPlatform.SelectedIndex;
318 | }
319 |
320 | private void gridAvailableVersions_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
321 | {
322 | // check that we clicked actually on a row
323 | var src = VisualTreeHelper.GetParent((DependencyObject)e.OriginalSource);
324 | var srcType = src.GetType();
325 | if (srcType == typeof(ContentPresenter))
326 | {
327 | BtnCreateNewProject_Click(null, null);
328 | }
329 | }
330 |
331 | private void chkForceDX11_Checked(object sender, RoutedEventArgs e)
332 | {
333 | forceDX11 = chkForceDX11.IsChecked == true;
334 | }
335 | }
336 | }
337 |
--------------------------------------------------------------------------------
/UnityLauncherPro/ProjectProperties.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
26 |
27 |
28 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/UnityLauncherPro/ProjectProperties.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Input;
8 |
9 | namespace UnityLauncherPro
10 | {
11 | ///
12 | /// Interaction logic for ProjectProperties.xaml
13 | ///
14 | public partial class ProjectProperties : Window
15 | {
16 | Project proj;
17 |
18 | public ProjectProperties(Project proj)
19 | {
20 | this.proj = proj;
21 | InitializeComponent();
22 | }
23 |
24 | private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
25 | {
26 |
27 | }
28 |
29 | private void btnCloseProperties_Click(object sender, RoutedEventArgs e)
30 | {
31 | DialogResult = false;
32 | }
33 |
34 | private void txtCustomEnvVariables_PreviewKeyDown(object sender, KeyEventArgs e)
35 | {
36 |
37 | }
38 |
39 | private void txtCustomEnvVariables_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
40 | {
41 | // TODO validate
42 | }
43 |
44 | private void btnApplyProperties_Click(object sender, RoutedEventArgs e)
45 | {
46 | DialogResult = true;
47 |
48 | // TODO save settings to usersettings folder
49 | Tools.SaveProjectSettings(proj, txtCustomEnvVariables.Text);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("UnityLauncherPro")]
11 | [assembly: AssemblyDescription("Unity Hub Alternative")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("UnityCoder.com")]
14 | [assembly: AssemblyProduct("UnityLauncherPro")]
15 | [assembly: AssemblyCopyright("Copyright © 2025")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.0.0.1")]
55 | [assembly: AssemblyFileVersion("1.0.0.1")]
56 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace UnityLauncherPro.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("UnityLauncherPro.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 880
7 |
8 |
9 | 650
10 |
11 |
12 | True
13 |
14 |
15 | False
16 |
17 |
18 | False
19 |
20 |
21 | True
22 |
23 |
24 | False
25 |
26 |
27 | False
28 |
29 |
30 |
31 |
32 |
33 | False
34 |
35 |
36 | True
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | True
46 |
47 |
48 | True
49 |
50 |
51 | False
52 |
53 |
54 | True
55 |
56 |
57 |
58 |
59 |
60 | False
61 |
62 |
63 | theme.ini
64 |
65 |
66 | True
67 |
68 |
69 | False
70 |
71 |
72 | False
73 |
74 |
75 | dd/MM/yyyy HH:mm:ss
76 |
77 |
78 | True
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | False
88 |
89 |
90 | -s Unity ActivityManager PackageManager dalvikvm DEBUG -v color
91 |
92 |
93 | 0
94 |
95 |
96 |
97 |
98 |
99 | win64
100 |
101 |
102 | False
103 |
104 |
105 | False
106 |
107 |
108 |
109 |
110 |
111 | <?xml version="1.0" encoding="utf-16"?>
112 | <ArrayOfString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
113 | <string>C:\Program Files\Unity\Hub\Editor\</string>
114 | <string>C:\Program Files\</string>
115 | </ArrayOfString>
116 |
117 |
118 | False
119 |
120 |
121 | 50000
122 |
123 |
124 | <?xml version="1.0" encoding="utf-16"?>
125 | <ArrayOfString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
126 |
127 |
128 | True
129 |
130 |
131 | 50
132 |
133 |
134 | https://raw.githubusercontent.com/unitycoder/UnityInitializeProject/main/Assets/Editor/InitializeProject.cs
135 |
136 |
137 |
138 |
139 |
140 | True
141 |
142 |
143 | False
144 |
145 |
146 | True
147 |
148 |
149 | False
150 |
151 |
152 | False
153 |
154 |
155 | False
156 |
157 |
158 | False
159 |
160 |
161 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Resources/Colors.xaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/UnityLauncherPro/ThemeEditor.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
80 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/UnityLauncherPro/ThemeEditor.xaml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Win32;
2 | using System;
3 | using System.Collections;
4 | using System.Collections.Generic;
5 | using System.Collections.ObjectModel;
6 | using System.ComponentModel;
7 | using System.IO;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 |
13 | namespace UnityLauncherPro
14 | {
15 | public partial class ThemeEditor : Window
16 | {
17 | static ObservableCollection themeColors = new ObservableCollection();
18 | static ObservableCollection themeColorsOrig = new ObservableCollection();
19 |
20 | string previousSaveFileName = null;
21 |
22 | // hack for adjusting slider, without triggering onchange..
23 | bool forceValue = false;
24 |
25 | // for single undo
26 | Slider previousSlider;
27 | int previousValue = -1;
28 |
29 | public ThemeEditor()
30 | {
31 | InitializeComponent();
32 | }
33 |
34 | private void Window_Loaded(object sender, RoutedEventArgs e)
35 | {
36 | themeColors.Clear();
37 | themeColorsOrig.Clear();
38 |
39 | // get original colors to collection
40 | foreach (DictionaryEntry item in Application.Current.Resources.MergedDictionaries[0])
41 | {
42 | // take currently used colors
43 | var currentColor = (SolidColorBrush)Application.Current.Resources[item.Key];
44 |
45 | var themeColorPair = new ThemeColor();
46 | themeColorPair.Key = item.Key.ToString();
47 | themeColorPair.Brush = currentColor;
48 | themeColors.Add(themeColorPair);
49 |
50 | // take backup copy
51 | var themeColorPair2 = new ThemeColor();
52 | themeColorPair2.Key = item.Key.ToString();
53 | themeColorPair2.Brush = currentColor;
54 | themeColorsOrig.Add(themeColorPair2);
55 | }
56 | // display current theme keys and values
57 | gridThemeColors.ItemsSource = themeColors;
58 |
59 | // sort by key sa default
60 | gridThemeColors.Items.SortDescriptions.Add(new SortDescription("Key", ListSortDirection.Ascending));
61 |
62 | gridThemeColors.SelectedIndex = 0;
63 | }
64 |
65 | void UpdateColorPreview()
66 | {
67 | var newColor = new Color();
68 | newColor.R = (byte)sliderRed.Value;
69 | newColor.G = (byte)sliderGreen.Value;
70 | newColor.B = (byte)sliderBlue.Value;
71 | newColor.A = (byte)sliderAlpha.Value;
72 | var newColorBrush = new SolidColorBrush(newColor);
73 | rectSelectedColor.Fill = newColorBrush;
74 |
75 | // set new color into our collection values
76 | themeColors[themeColors.IndexOf((ThemeColor)gridThemeColors.SelectedItem)].Brush = newColorBrush;
77 |
78 | gridThemeColors.Items.Refresh();
79 |
80 | // apply color changes to mainwindow
81 | var item = gridThemeColors.SelectedItem as ThemeColor;
82 | Application.Current.Resources[item.Key] = newColorBrush;
83 | forceValue = false;
84 | }
85 |
86 | void SetSlider(Slider target, double color)
87 | {
88 | forceValue = true;
89 | target.Value = color;
90 | forceValue = false;
91 | }
92 |
93 | private void GridThemeColors_SelectionChanged(object sender, SelectionChangedEventArgs e)
94 | {
95 | if (gridThemeColors.SelectedIndex == -1) return;
96 |
97 | var item = gridThemeColors.SelectedItem as ThemeColor;
98 | if (item == null) return;
99 |
100 | // update preview box
101 | rectSelectedColor.Fill = item.Brush;
102 |
103 | // update RGBA sliders
104 | SetSlider(sliderRed, item.Brush.Color.R);
105 | SetSlider(sliderGreen, item.Brush.Color.G);
106 | SetSlider(sliderBlue, item.Brush.Color.B);
107 | SetSlider(sliderAlpha, item.Brush.Color.A);
108 | }
109 |
110 | private void BtnSaveTheme_Click(object sender, RoutedEventArgs e)
111 | {
112 | // 1) Determine the default filename (with .ini)
113 | string defaultName = string.IsNullOrEmpty(previousSaveFileName)
114 | ? "custom.ini"
115 | : previousSaveFileName + ".ini";
116 |
117 | // 2) Ask the helper for a safe full path
118 | string initialFullPath = Tools.GetSafeFilePath("Themes", defaultName);
119 | string initialDir = Path.GetDirectoryName(initialFullPath);
120 | string initialFile = Path.GetFileNameWithoutExtension(initialFullPath);
121 |
122 | // 3) Configure the save dialog
123 | var saveFileDialog = new SaveFileDialog
124 | {
125 | FileName = initialFile, // no extension here
126 | DefaultExt = ".ini",
127 | Filter = "Theme files (.ini)|*.ini",
128 | InitialDirectory = initialDir,
129 | RestoreDirectory = true
130 | };
131 |
132 | // 4) Show and, if confirmed, write out the INI
133 | if (saveFileDialog.ShowDialog() == true)
134 | {
135 | // Build INI lines
136 | var iniRows = new List
137 | {
138 | "# Created with UnityLauncherPro built-in theme editor "
139 | + DateTime.Now.ToString("dd/MM/yyyy")
140 | };
141 | // original-style loop
142 | for (int i = 0; i < themeColors.Count; i++)
143 | {
144 | iniRows.Add(themeColors[i].Key + "=" + themeColors[i].Brush.ToString());
145 | }
146 |
147 | // Get the chosen path & ensure its folder exists
148 | string themePath = saveFileDialog.FileName;
149 | previousSaveFileName = Path.GetFileNameWithoutExtension(themePath);
150 | Directory.CreateDirectory(Path.GetDirectoryName(themePath));
151 |
152 | // Write out
153 | File.WriteAllLines(themePath, iniRows);
154 | Console.WriteLine("Saved theme: " + themePath);
155 | }
156 | }
157 |
158 | private void BtnResetTheme_Click(object sender, RoutedEventArgs e)
159 | {
160 | for (int i = 0; i < themeColorsOrig.Count; i++)
161 | {
162 | // reset collection colors
163 | themeColors[i].Brush = themeColorsOrig[i].Brush;
164 |
165 | // reset application colors
166 | Application.Current.Resources[themeColors[i].Key] = themeColorsOrig[i].Brush;
167 | }
168 |
169 | // reset current color
170 | if (gridThemeColors.SelectedItem != null)
171 | {
172 | var item = gridThemeColors.SelectedItem as ThemeColor;
173 | SetSlider(sliderRed, item.Brush.Color.R);
174 | SetSlider(sliderGreen, item.Brush.Color.G);
175 | SetSlider(sliderBlue, item.Brush.Color.B);
176 | SetSlider(sliderAlpha, item.Brush.Color.A);
177 | }
178 |
179 | UpdateColorPreview();
180 | gridThemeColors.Items.Refresh();
181 | }
182 |
183 | private void SliderRed_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
184 | {
185 | // onchanged is called before other components are ready..thanks wpf :D
186 | if (forceValue == true || txtRed == null) return;
187 | UpdateColorPreview();
188 | }
189 |
190 | private void SliderGreen_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
191 | {
192 | if (forceValue == true || txtGreen == null) return;
193 | UpdateColorPreview();
194 | }
195 |
196 | private void SliderBlue_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
197 | {
198 | if (forceValue == true || txtBlue == null) return;
199 | UpdateColorPreview();
200 | }
201 |
202 | public void Executed_Undo(object sender, ExecutedRoutedEventArgs e)
203 | {
204 | // restore previous color
205 | SetSlider(previousSlider, previousValue);
206 | UpdateColorPreview();
207 | }
208 |
209 | public void CanExecute_Undo(object sender, CanExecuteRoutedEventArgs e)
210 | {
211 | e.CanExecute = previousValue > -1;
212 | }
213 |
214 | private void SliderAlpha_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
215 | {
216 | if (forceValue == true) return;
217 | if (txtAlpha == null) return;
218 | UpdateColorPreview();
219 | }
220 |
221 | public void Executed_Save(object sender, ExecutedRoutedEventArgs e)
222 | {
223 | BtnSaveTheme_Click(null, null);
224 | }
225 |
226 | public void CanExecute_Save(object sender, CanExecuteRoutedEventArgs e)
227 | {
228 | e.CanExecute = true;
229 | }
230 |
231 | private void SliderRed_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
232 | {
233 | SetUndoValues(sender, txtRed);
234 | }
235 |
236 | private void SliderGreen_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
237 | {
238 | SetUndoValues(sender, txtGreen);
239 | }
240 |
241 | private void SliderBlue_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
242 | {
243 | SetUndoValues(sender, txtBlue);
244 | }
245 |
246 | private void SliderAlpha_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
247 | {
248 | SetUndoValues(sender, txtAlpha);
249 | }
250 |
251 | void SetUndoValues(Object sender, TextBox textBox)
252 | {
253 | previousSlider = (Slider)sender;
254 | previousValue = (int)previousSlider.Value;
255 | }
256 |
257 | private void TxtRed_KeyUp(object sender, KeyEventArgs e)
258 | {
259 | GetColorFromTextBox((TextBox)sender, sliderRed);
260 | }
261 |
262 | private void TxtGreen_KeyUp(object sender, KeyEventArgs e)
263 | {
264 | GetColorFromTextBox((TextBox)sender, sliderGreen);
265 | }
266 |
267 | private void TxtBlue_KeyUp(object sender, KeyEventArgs e)
268 | {
269 | GetColorFromTextBox((TextBox)sender, sliderBlue);
270 | }
271 |
272 | private void TxtAlpha_KeyUp(object sender, KeyEventArgs e)
273 | {
274 | GetColorFromTextBox((TextBox)sender, sliderAlpha);
275 | }
276 |
277 | void GetColorFromTextBox(TextBox source, Slider target)
278 | {
279 | int col = 0;
280 | if (int.TryParse(source.Text, out col))
281 | {
282 | bool overWrite = false;
283 | if (col < 0) { col = 0; overWrite = true; }
284 | if (col > 255) { col = 255; overWrite = true; }
285 |
286 | source.Text = col + "";
287 | target.Value = col;
288 |
289 | if (overWrite == true) source.SelectAll();
290 | }
291 | }
292 |
293 | private void TxtColorField_PreviewKeyDown(object sender, KeyEventArgs e)
294 | {
295 | switch (e.Key)
296 | {
297 | case Key.Escape: // undo current textbox edit
298 | ((TextBox)sender).Undo();
299 | break;
300 | }
301 | }
302 | } // class
303 | } // namespace
--------------------------------------------------------------------------------
/UnityLauncherPro/UnityLauncherPro.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {EC78D91A-3E63-4CAA-8BC3-9673A30FDA45}
8 | WinExe
9 | UnityLauncherPro
10 | UnityLauncherPro
11 | v4.8
12 | 512
13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
14 | 4
15 | true
16 | true
17 | publish\
18 | true
19 | Disk
20 | false
21 | Foreground
22 | 7
23 | Days
24 | false
25 | false
26 | true
27 | 0
28 | 1.0.0.%2a
29 | false
30 | false
31 | true
32 |
33 |
34 |
35 | AnyCPU
36 | true
37 | full
38 | false
39 | bin\Debug\
40 | DEBUG;TRACE
41 | prompt
42 | 4
43 |
44 |
45 | AnyCPU
46 | pdbonly
47 | true
48 | bin\Release\
49 | TRACE
50 | prompt
51 | 4
52 |
53 |
54 | Images/icon.ico
55 |
56 |
57 |
58 | app.manifest
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | 4.0
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | MSBuild:Compile
84 | Designer
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | DownloadProgressWindow.xaml
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | NewProject.xaml
109 |
110 |
111 | ProjectProperties.xaml
112 |
113 |
114 | ThemeEditor.xaml
115 |
116 |
117 |
118 |
119 | UpgradeWindow.xaml
120 |
121 |
122 |
123 |
124 | Designer
125 | MSBuild:Compile
126 |
127 |
128 | Designer
129 | MSBuild:Compile
130 |
131 |
132 | Designer
133 | MSBuild:Compile
134 |
135 |
136 | MSBuild:Compile
137 | Designer
138 |
139 |
140 | App.xaml
141 | Code
142 |
143 |
144 | MainWindow.xaml
145 | Code
146 |
147 |
148 | Designer
149 | MSBuild:Compile
150 |
151 |
152 | Designer
153 | MSBuild:Compile
154 |
155 |
156 |
157 |
158 |
159 | Code
160 |
161 |
162 | True
163 | True
164 | Resources.resx
165 |
166 |
167 | True
168 | Settings.settings
169 | True
170 |
171 |
172 | ResXFileCodeGenerator
173 | Resources.Designer.cs
174 |
175 |
176 |
177 | SettingsSingleFileGenerator
178 | Settings.Designer.cs
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | False
187 | Microsoft .NET Framework 4.6.1 %28x86 and x64%29
188 | true
189 |
190 |
191 | False
192 | .NET Framework 3.5 SP1
193 | false
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
--------------------------------------------------------------------------------
/UnityLauncherPro/UpgradeWindow.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
26 |
29 |
32 |
33 |
34 |
37 |
40 |
41 |
48 |
49 |
50 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/UnityLauncherPro/UpgradeWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Input;
5 | using System.Windows.Media;
6 |
7 | namespace UnityLauncherPro
8 | {
9 | ///
10 | /// Interaction logic for UpgradeWindow.xaml
11 | ///
12 | public partial class UpgradeWindow : Window
13 | {
14 | public static string upgradeVersion = null;
15 |
16 | public UpgradeWindow(string currentVersion, string projectPath, string commandLineArguments = null)
17 | {
18 | InitializeComponent();
19 | txtCurrentVersion.Text = currentVersion;
20 | txtCurrentPlatform.Text = Tools.GetTargetPlatform(projectPath);
21 |
22 | if (gridAvailableVersions.ItemsSource == null)
23 | {
24 | gridAvailableVersions.ItemsSource = MainWindow.unityInstallationsSource;
25 | }
26 |
27 | gridAvailableVersions.SelectedItem = null;
28 |
29 | // we have current version info in project
30 | // enable release and dl buttons
31 | btnOpenReleasePage.IsEnabled = true;
32 | btnDownload.IsEnabled = true;
33 |
34 | // if dont have exact version, show red outline
35 | if (currentVersion == null || MainWindow.unityInstalledVersions.ContainsKey(currentVersion) == false)
36 | {
37 | txtCurrentVersion.BorderBrush = Brushes.Red;
38 | txtCurrentVersion.BorderThickness = new Thickness(1);
39 | }
40 |
41 | if (currentVersion != null)
42 | {
43 | // remove china c1 from version
44 | if (currentVersion.Contains("c")) currentVersion = currentVersion.Replace("c1", "");
45 | // find nearest version
46 | string nearestVersion = Tools.FindNearestVersion(currentVersion, MainWindow.unityInstalledVersions.Keys.ToList());
47 |
48 | if (nearestVersion != null)
49 | {
50 | // select nearest version
51 | for (int i = 0; i < MainWindow.unityInstallationsSource.Count; i++)
52 | {
53 | if (MainWindow.unityInstallationsSource[i].Version == nearestVersion)
54 | {
55 | gridAvailableVersions.SelectedIndex = i;
56 | gridAvailableVersions.ScrollIntoView(gridAvailableVersions.SelectedItem);
57 | break;
58 | }
59 | }
60 | }
61 | }
62 |
63 | gridAvailableVersions.Focus();
64 | }
65 |
66 |
67 | private void BtnUpgradeProject_Click(object sender, RoutedEventArgs e)
68 | {
69 | Upgrade();
70 | }
71 |
72 | private void BtnCancelUpgrade_Click(object sender, RoutedEventArgs e)
73 | {
74 | DialogResult = false;
75 | }
76 |
77 | private void BtnOpenReleasePage_Click(object sender, RoutedEventArgs e)
78 | {
79 | Tools.OpenReleaseNotes(txtCurrentVersion.Text);
80 | }
81 |
82 |
83 | private void BtnDownloadEditor_Click(object sender, RoutedEventArgs e)
84 | {
85 | Tools.DownloadInBrowser(txtCurrentVersion.Text);
86 | }
87 |
88 | private void BtnDownload_Click(object sender, RoutedEventArgs e)
89 | {
90 | Tools.DownloadInBrowser(txtCurrentVersion.Text);
91 | }
92 |
93 | private void btnInstall_Click(object sender, RoutedEventArgs e)
94 | {
95 | Tools.DownloadAndInstall(txtCurrentVersion.Text);
96 | }
97 |
98 | private void Window_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
99 | {
100 | // override Enter for datagrid
101 | if (e.Key == Key.Return && e.KeyboardDevice.Modifiers == ModifierKeys.None)
102 | {
103 | e.Handled = true;
104 | var k = (UnityInstallation)gridAvailableVersions.SelectedItem;
105 | upgradeVersion = k.Version;
106 | DialogResult = true;
107 | return;
108 | }
109 | else // other keys
110 | {
111 | switch (e.Key)
112 | {
113 | case Key.Escape:
114 | DialogResult = false;
115 | break;
116 | default:
117 | break;
118 | }
119 | }
120 |
121 | base.OnKeyDown(e);
122 | }
123 |
124 | private void GridAvailableVersions_PreviewKeyDown(object sender, KeyEventArgs e)
125 | {
126 | Tools.HandleDataGridScrollKeys(sender, e);
127 | }
128 |
129 | private void GridAvailableVersions_Loaded(object sender, RoutedEventArgs e)
130 | {
131 | Tools.SetFocusToGrid(gridAvailableVersions);
132 |
133 | // bolded for current item
134 | DataGridRow row = (DataGridRow)((DataGrid)sender).ItemContainerGenerator.ContainerFromIndex(gridAvailableVersions.SelectedIndex);
135 | if (row == null) return;
136 | row.Foreground = Brushes.White;
137 | row.FontWeight = FontWeights.Bold;
138 | }
139 |
140 | private void GridAvailableVersions_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
141 | {
142 | var src = VisualTreeHelper.GetParent((DependencyObject)e.OriginalSource);
143 | var srcType = src.GetType();
144 | if (srcType == typeof(ContentPresenter))
145 | {
146 | Upgrade();
147 | }
148 | }
149 |
150 | void Upgrade()
151 | {
152 | var k = (UnityInstallation)gridAvailableVersions.SelectedItem;
153 | upgradeVersion = k.Version;
154 | DialogResult = true;
155 | }
156 |
157 |
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/UnityLauncherPro/Version.cs:
--------------------------------------------------------------------------------
1 | namespace UnityLauncherPro
2 | {
3 | internal class Version
4 | {
5 | public static string Stamp = "";
6 | }
7 | }
--------------------------------------------------------------------------------
/UnityLauncherPro/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
60 | PerMonitorV2
61 | true
62 |
63 |
64 |
65 |
66 |
67 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 1.0.{build}
2 | skip_tags: true
3 | image: Visual Studio 2022
4 | configuration: Release
5 | only_commits:
6 | message: /#build/
7 | build:
8 | verbosity: minimal
9 | after_build:
10 | - cmd: 7z a UnityLauncherPro.zip %APPVEYOR_BUILD_FOLDER%\UnityLauncherPro\bin\Release\*.exe %APPVEYOR_BUILD_FOLDER%\UnityLauncherPro\bin\Release\*.exe.config
11 | artifacts:
12 | - path: UnityLauncherPro.zip
13 | name: deploy
14 | deploy:
15 | - provider: GitHub
16 | auth_token:
17 | secure: kmYSrl7Mx/PFDGcyC5gS/vpW2UJCVguEXZsQ0LtkfwmSzx+3noZOyPrbZQ8uWX2B
18 | artifact: deploy
19 |
--------------------------------------------------------------------------------