Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions UnityExtension/Commands/OpenUnityCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public OpenUnityCommand(UnityProject project)

public override CommandResult Invoke()
{
// Dead reference: the project's folder no longer exists on disk. Don't try to
// launch Unity against a missing path (also guards against the folder being
// deleted after the list was built).
if (!Directory.Exists(_projectPath))
{
return CommandResult.ShowToast($"Project folder no longer exists:\n{_projectPath}");
}

// Lazy-load only when invoking
_editorPath ??= EditorParser.GetExecutablePathForVersion(_projectVersion);

Expand Down
6 changes: 6 additions & 0 deletions UnityExtension/Data/UnityProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ internal sealed record UnityProject
public string Version { get; init; } = string.Empty;
public long LastModified { get; init; }
public bool IsFavorite { get; init; }

/// <summary>
/// Whether the project's folder still exists on disk. Dead references
/// (moved/deleted projects still listed by Unity Hub) have this set to false.
/// </summary>
public bool Exists { get; init; }
}
127 changes: 100 additions & 27 deletions UnityExtension/Helpers/ProjectParser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
Expand All @@ -9,49 +9,122 @@ public static class ProjectParser
{
internal static List<UnityProject> GetUnityProjects()
{
var result = new List<UnityProject>();
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var projectsJsonFilePath = Path.Combine(appDataPath, Resources.ProjectsJsonPath);
var projectsJsonFilePath = Resources.ProjectsJsonPath;

if (!File.Exists(projectsJsonFilePath))
{
return result;
return new List<UnityProject>();
}

string jsonContent;
try
{
var jsonContent = File.ReadAllText(projectsJsonFilePath);
var projectsRoot = JsonDocument.Parse(jsonContent).RootElement;
jsonContent = File.ReadAllText(projectsJsonFilePath);
}
catch (Exception)
{
// File exists but couldn't be read (locked, permissions, etc.)
return new List<UnityProject>();
}

return ParseProjects(jsonContent);
}

/// <summary>
/// Parses the contents of Unity Hub's projects-v1.json into a list of projects.
/// Each entry is parsed independently: a single malformed or incomplete entry
/// (e.g. a dead reference to a moved/deleted project) is skipped rather than
/// aborting the whole list.
/// </summary>
internal static List<UnityProject> ParseProjects(string jsonContent)
{
var result = new List<UnityProject>();

JsonElement projectsRoot;
try
{
projectsRoot = JsonDocument.Parse(jsonContent).RootElement;
}
catch (JsonException)
{
// Corrupt/unreadable JSON → empty list; the page shows "No projects found".
return result;
}

if (
!projectsRoot.TryGetProperty("data", out var data)
|| data.ValueKind != JsonValueKind.Object
)
{
return result;
}

if (projectsRoot.TryGetProperty("data", out var data))
foreach (var property in data.EnumerateObject())
{
try
{
foreach (var property in data.EnumerateObject())
var projectPath = property.Name;
var projectInfo = property.Value;

var fallbackTitle = Path.GetFileName(projectPath.TrimEnd('/', '\\'));

var title =
projectInfo.TryGetProperty("title", out var titleEl)
&& titleEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrEmpty(titleEl.GetString())
? titleEl.GetString()!
: fallbackTitle;

var version =
projectInfo.TryGetProperty("version", out var versionEl)
&& versionEl.ValueKind == JsonValueKind.String
? versionEl.GetString()!
: "Unknown";

long lastModified = 0;
if (
projectInfo.TryGetProperty("lastModified", out var lastModifiedEl)
&& lastModifiedEl.ValueKind == JsonValueKind.Number
&& lastModifiedEl.TryGetInt64(out var parsedLastModified)
)
{
var projectPath = property.Name;
var projectInfo = property.Value;
lastModified = parsedLastModified;
}

var isFavorite =
projectInfo.TryGetProperty("isFavorite", out var isFavoriteEl)
&& isFavoriteEl.ValueKind == JsonValueKind.True;

var project = new UnityProject
result.Add(
new UnityProject
{
Path = projectPath,
Title =
projectInfo.GetProperty("title").GetString()
?? Path.GetFileName(projectPath),
Version = projectInfo.GetProperty("version").GetString() ?? "Unknown",
LastModified = projectInfo.GetProperty("lastModified").GetInt64(),
IsFavorite =
projectInfo.TryGetProperty("isFavorite", out var isFavorite)
&& isFavorite.GetBoolean(),
};

result.Add(project);
}
Title = title,
Version = version,
LastModified = lastModified,
IsFavorite = isFavorite,
Exists = SafeDirectoryExists(projectPath),
}
);
}
catch (Exception)
{
// Skip only this malformed entry; keep parsing the rest.
}
}

return result;
}

private static bool SafeDirectoryExists(string path)
{
try
{
return !string.IsNullOrEmpty(path) && Directory.Exists(path);
}
catch (Exception)
{
// Any error in parsing will result in an empty list
return false;
}

return result;
}
}
4 changes: 4 additions & 0 deletions UnityExtension/Helpers/Resources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ public static class Resources
);

public static IconInfo IconUrl => new("\uE8A7");

// Segoe Fluent "Warning" glyph \u2014 used to flag projects whose folder is gone from disk.
public static IconInfo IconMissing => new("\uE7BA");

public static IconInfo IconUnity => IconHelpers.FromRelativePath("Assets\\UnityLogo.png");
}
11 changes: 11 additions & 0 deletions UnityExtension/Helpers/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ internal sealed class SettingsManager : JsonSettingsManager
private ToggleSetting GroupFavoritesFirstSetting { get; }
public bool GroupFavoritesFirst => GroupFavoritesFirstSetting.Value;

private ToggleSetting HideMissingProjectsSetting { get; }
public bool HideMissingProjects => HideMissingProjectsSetting.Value;

private static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("UnityExtension");
Expand All @@ -27,7 +30,15 @@ public SettingsManager()
defaultValue: true
);

HideMissingProjectsSetting = new ToggleSetting(
key: "hideMissingProjects",
label: "Hide projects missing from disk",
description: "Don't list Unity projects whose folder no longer exists on disk",
defaultValue: false
);

Settings.Add(GroupFavoritesFirstSetting);
Settings.Add(HideMissingProjectsSetting);

LoadSettings();

Expand Down
25 changes: 16 additions & 9 deletions UnityExtension/Pages/UnityExtensionPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public override IListItem[] GetItems()
{
var projects = ProjectParser.GetUnityProjects();

if (_settingsManager.HideMissingProjects)
{
projects = projects.Where(p => p.Exists).ToList();
}

if (projects.Count == 0)
{
items.Add(
Expand All @@ -50,14 +55,11 @@ public override IListItem[] GetItems()
{
projects.Sort((a, b) => b.LastModified.CompareTo(a.LastModified));

if (_settingsManager.GroupFavoritesFirst)
{
var favoriteProjects = projects.Where(p => p.IsFavorite).ToList();
var nonFavoriteProjects = projects.Where(p => !p.IsFavorite).ToList();
IEnumerable<UnityProject> ordered = _settingsManager.GroupFavoritesFirst
? projects.Where(p => p.IsFavorite).Concat(projects.Where(p => !p.IsFavorite))
: projects;

items.AddRange(favoriteProjects.Select(CreateProjectListItem));
items.AddRange(nonFavoriteProjects.Select(CreateProjectListItem));
}
items.AddRange(ordered.Select(CreateProjectListItem));
}
}
catch (Exception ex)
Expand Down Expand Up @@ -94,13 +96,18 @@ private static ListItem CreateProjectListItem(UnityProject project)
tags.Add(new Tag("⭐ Favorite") { Foreground = ColorHelpers.FromRgb(222, 186, 56) });
}

if (!project.Exists)
{
tags.Add(new Tag("Missing") { Foreground = ColorHelpers.FromRgb(224, 108, 117) });
}

tags.Add(new Tag(project.Version));

return new ListItem(defaultCommand)
{
Title = project.Title,
Subtitle = project.Path,
Icon = Resources.IconUnity,
Subtitle = project.Exists ? project.Path : $"{project.Path} (folder not found)",
Icon = project.Exists ? Resources.IconUnity : Resources.IconMissing,
Tags = tags.ToArray(),
MoreCommands = contextCommands.ToArray(),
};
Expand Down