Skip to content
Merged
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
52 changes: 52 additions & 0 deletions plugins/unity-launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Unity 启动器 (zTools 插件)

![Unity Launcher](logo.png)

**Unity 启动器** 是一款专为 Unity 开发者打造的高效项目与编辑器管理插件。无需依赖体积庞大的 Unity Hub,即可轻松实现 Unity 项目的批量扫描识别、多版本编辑器的智能匹配与一键快速启动。

---

## ✨ 核心特性

- 🚀 **脱离 Unity Hub 依赖**
轻量极速,直接关联本机已安装的 `Unity.exe`,一键打开项目,拒绝臃肿。

- 🔍 **深度递归项目扫描**
- 支持选择单个项目文件夹、父级目录甚至整个磁盘根目录。
- 内置高性能**非阻塞异步扫描引擎**,支持多层级深度递归查找。
- 拥有智能白名单与目录黑名单规则(自动跳过 `node_modules`、`Library`、`Windows` 等冗余及系统受保护目录)。
- **实时进度反馈**:扫描过程中提供可视化模态框,实时显示已扫描目录数与已识别项目数,界面流畅不卡顿。

- 🛠️ **编辑器版本智能匹配**
- 自动读取项目的 `ProjectSettings/ProjectVersion.txt` 识别所需 Unity 版本。
- **精确匹配优先**:自动调用版本完全一致的 `Unity.exe`。
- **智能版本回退**:若未精准匹配,优先选择最兼容的最新相近大版本编辑器打开。
- 支持为特定项目单独绑定或切换指定的 `Unity.exe`。

- ⚡ **自动扫描本机 Unity 编辑器**
自动检索常见安装路径(如 `C:/Program Files/Unity/Hub/Editor` 等),快速导入所有本地 Unity 编辑器,支持版本号解析与版本自然排序。

- 📌 **快捷管理与丰富操作**
- 支持项目置顶、快速搜索、备注说明添加。
- 支持在文件资源管理器中打开、快捷清理本地项目记录及彻底删除项目文件夹等功能。

- ⌨️ **快捷唤起指令**
在 zTools 搜索框输入以下关键字均可唤起插件:`ul` | `unity` | `unity启动器` | `Unity项目`

---

## 📖 使用指南

### 1. 批量添加 Unity 项目
1. 点击顶部栏右侧的 `+ 添加项目` 按钮。
2. 在弹出框中选择任意包含 Unity 项目的文件夹或磁盘盘符(例如 `D:\`)。
3. 插件将自动开启后台异步扫描,并在界面中实时展示扫描进度。扫描完成后即可自动导入所有新发现的项目。

### 2. 管理 Unity 编辑器
1. 切换至 **“编辑器管理”** 选项卡。
2. 点击 `自动扫描`,插件会自动检索常见安装路径下的 `Unity.exe`。
3. 也可以点击 `添加编辑器` 手动选择指定的 `Unity.exe` 可执行文件。

### 3. 打开与配置项目
- **默认打开**:直接点击项目卡片上的 `打开项目` 按钮,插件将自动寻找最匹配的 Unity 编辑器启动。
- **关联特定版本**:点击项目卡片底部的 `版本/编辑器设置`,可手动为该项目指定固定的 `Unity.exe`。
70 changes: 61 additions & 9 deletions plugins/unity-launcher/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,63 @@ function renderTabs() {
};
}

function addItem() {
async function addItem() {
if (state.tab === 'projects') {
const list = services.pickFolder();
if (!list || !list.length) return;

const selectedDir = services.selectFolder ? services.selectFolder() : null;
if (!selectedDir) return;

const progressHtml = `
<div style="padding: 10px 0; text-align: center;">
<div style="font-weight: 600; color: #6366f1; margin-bottom: 6px; font-size: 13px;">正在深度扫描 Unity 项目,请稍候...</div>
<div id="scanCurrentDirText" style="font-size: 11px; color: var(--text-secondary); margin-bottom: 14px; word-break: break-all; opacity: 0.85; font-family: monospace; height: 32px; overflow: hidden; display: flex; align-items: center; justify-content: center; padding: 0 10px;">${esc(selectedDir)}</div>
<div class="progress-container">
<div class="progress-track">
<div class="progress-bar-fill progress-bar-animated" style="background: linear-gradient(90deg, #6366f1 0%, #3b82f6 50%, #10b981 100%);"></div>
</div>
<div class="progress-status-text" style="margin-top: 10px; display: flex; justify-content: space-between; padding: 0 4px;">
<span id="scanProgressDirCount">已扫描目录: 0</span>
<span id="scanProgressFoundCount" style="color: #10b981; font-weight: 600;">已识别项目: 0 个</span>
</div>
</div>
</div>
`;

openModal({
title: '🔍 批量扫描 Unity 项目',
content: progressHtml,
isHtml: true,
showFooter: false
});

const dirTextEl = document.getElementById('scanCurrentDirText');
const dirCountEl = document.getElementById('scanProgressDirCount');
const foundCountEl = document.getElementById('scanProgressFoundCount');

let list = [];
try {
if (services.scanFolderAsync) {
list = await services.scanFolderAsync(selectedDir, ({ scannedCount, foundCount, currentDir }) => {
if (dirTextEl) dirTextEl.textContent = currentDir;
if (dirCountEl) dirCountEl.textContent = `已扫描目录: ${scannedCount}`;
if (foundCountEl) foundCountEl.textContent = `已识别项目: ${foundCount} 个`;
});
} else {
list = services.pickFolder() || [];
}
} catch (err) {
console.error('Scan failed:', err);
} finally {
const overlay = document.getElementById('modalOverlay');
if (overlay) overlay.classList.remove('active');
}

if (!list || !list.length) {
if (ztools.showNotification) {
ztools.showNotification('没有检测到新的 Unity 项目');
}
return;
}

let addedCount = 0;
list.forEach(r => {
if (state.projects.some(p => p.path === r.path)) return;
Expand All @@ -195,25 +247,25 @@ function addItem() {
});
addedCount++;
});

if (ztools.showNotification) {
if (addedCount > 0) {
ztools.showNotification(`成功添加 ${addedCount} 个 Unity 项目`);
ztools.showNotification(`成功识别并添加 ${addedCount} 个 Unity 项目`);
} else {
ztools.showNotification('没有检测到新的 Unity 项目或项目已存在');
ztools.showNotification('未扫描到新的 Unity 项目或项目已存在');
}
}
} else {
const list = services.pickExe();
if (!list || !list.length) return;

let addedCount = 0;
list.forEach(e => {
if (state.editors.some(x => x.path === e.path)) return;
state.editors.push(e);
addedCount++;
});

if (ztools.showNotification) {
if (addedCount > 0) {
ztools.showNotification(`成功识别并添加 ${addedCount} 个 Unity 编辑器`);
Expand Down
14 changes: 11 additions & 3 deletions plugins/unity-launcher/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"title": "Unity启动器",
"pluginName": "Unity启动器",
"description": "独立管理 Unity 项目与编辑器版本,一键打开项目(不依赖 Unity Hub)",
"version": "1.0.0",
"version": "1.0.1",
"author": "zhanglei",
"homepage": "https://example.com",
"main": "index.html",
Expand All @@ -13,7 +13,15 @@
{
"code": "unity-launcher",
"explain": "打开 Unity 项目 / 管理编辑器版本",
"cmds": ["ul", "unity", "unity启动器", "Unity项目"]
"cmds": [
"ul",
"unity",
"unity启动器",
"Unity项目"
]
}
],
"platform": [
"win32"
]
}
}
164 changes: 139 additions & 25 deletions plugins/unity-launcher/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,30 @@ window.services = {
dbSet(key, val) { return ztools.dbStorage ? ztools.dbStorage.setItem(key, val) : null; },

// ---------- 文件/目录选择(支持单个/批量扫描) ----------
selectFolder() {
if (!ztools.showOpenDialog) return null;
const r = ztools.showOpenDialog({
title: '选择 Unity 项目文件夹(或选择磁盘根目录/父文件夹以批量扫描项目)',
properties: ['openDirectory'],
filters: [{ name: 'Unity 项目', extensions: ['*'] }]
});
if (!r || !r.length) return null;
return r[0];
},
scanFolderAsync(selectedPath, onProgress) {
return findUnityProjectsAsync(selectedPath, onProgress);
},
pickFolder() {
if (!ztools.showOpenDialog) return null;
const r = ztools.showOpenDialog({
title: '选择 Unity 项目文件夹(或选择其父文件夹以批量扫描项目)',
title: '选择 Unity 项目文件夹(或选择磁盘根目录/父文件夹以批量扫描项目)',
properties: ['openDirectory'],
filters: [{ name: 'Unity 项目', extensions: ['*'] }]
});
if (!r || !r.length) return null;
const selectedPath = r[0];

// 1. 检查选择 of the directory itself is a Unity project
const selfProj = validateProject(selectedPath);
if (selfProj.isUnityProject) {
return [selfProj];
}

// 2. If it is not a project itself, scan its first-level subdirectories
const subprojects = [];
const fs = require('fs');
const path = require('path');
try {
const files = fs.readdirSync(selectedPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
const subPath = path.join(selectedPath, file.name);
const subProj = validateProject(subPath);
if (subProj.isUnityProject) {
subprojects.push(subProj);
}
}
}
} catch (e) {}

return subprojects;
return findUnityProjects(selectedPath);
},
pickExe() {
if (!ztools.showOpenDialog) return null;
Expand Down Expand Up @@ -174,6 +164,130 @@ window.services = {
}
};

async function findUnityProjectsAsync(dir, onProgress, depth = 0, maxDepth = 8, state = { scannedCount: 0, found: [] }) {
const fs = require('fs');
const path = require('path');

state.scannedCount++;

if (state.scannedCount === 1 || state.scannedCount % 10 === 0) {
if (onProgress) {
onProgress({
scannedCount: state.scannedCount,
foundCount: state.found.length,
currentDir: dir
});
}
if (state.scannedCount % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}

// 1. 检查当前选择/搜索的目录自身是否为 Unity 项目
const selfProj = validateProject(dir);
if (selfProj.isUnityProject) {
state.found.push(selfProj);
if (onProgress) {
onProgress({
scannedCount: state.scannedCount,
foundCount: state.found.length,
currentDir: dir
});
}
return state.found;
}

if (depth >= maxDepth) return state.found;

try {
const files = await fs.promises.readdir(dir, { withFileTypes: true });
for (const file of files) {
if (file.isSymbolicLink && file.isSymbolicLink()) continue;

if (file.isDirectory()) {
const nameLower = file.name.toLowerCase();
if (
file.name.startsWith('.') ||
file.name.startsWith('$') ||
nameLower === 'node_modules' ||
nameLower === 'library' ||
nameLower === 'assets' ||
nameLower === 'projectsettings' ||
nameLower === 'temp' ||
nameLower === 'logs' ||
nameLower === 'obj' ||
nameLower === 'build' ||
nameLower === 'builds' ||
nameLower === 'windows' ||
nameLower === 'program files' ||
nameLower === 'program files (x86)' ||
nameLower === 'programdata' ||
nameLower === 'appdata' ||
nameLower === 'system volume information'
) {
continue;
}
const subPath = path.join(dir, file.name);
await findUnityProjectsAsync(subPath, onProgress, depth + 1, maxDepth, state);
}
}
} catch (e) {}

return state.found;
}

function findUnityProjects(dir, depth = 0, maxDepth = 8) {
const fs = require('fs');
const path = require('path');

// 1. 检查当前选择/搜索的目录自身是否为 Unity 项目
const selfProj = validateProject(dir);
if (selfProj.isUnityProject) {
return [selfProj];
}

if (depth >= maxDepth) return [];

let results = [];
try {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
if (file.isSymbolicLink && file.isSymbolicLink()) continue;

if (file.isDirectory()) {
const nameLower = file.name.toLowerCase();
if (
file.name.startsWith('.') ||
file.name.startsWith('$') ||
nameLower === 'node_modules' ||
nameLower === 'library' ||
nameLower === 'assets' ||
nameLower === 'projectsettings' ||
nameLower === 'temp' ||
nameLower === 'logs' ||
nameLower === 'obj' ||
nameLower === 'build' ||
nameLower === 'builds' ||
nameLower === 'windows' ||
nameLower === 'program files' ||
nameLower === 'program files (x86)' ||
nameLower === 'programdata' ||
nameLower === 'appdata' ||
nameLower === 'system volume information'
) {
continue;
}
const subPath = path.join(dir, file.name);
const subProjects = findUnityProjects(subPath, depth + 1, maxDepth);
if (subProjects.length > 0) {
results = results.concat(subProjects);
}
}
}
} catch (e) {}
return results;
}

function findUnityExes(dir, depth = 0) {
if (depth > 3) return []; // 限制深度为 3 层,避免在大目录下卡死
const fs = require('fs');
Expand Down
Loading