143 lines
5.5 KiB
JavaScript
143 lines
5.5 KiB
JavaScript
// 进度弹窗:用 Eagle 自己的 create-plugin-window 通道开一个正规插件窗口。
|
|
//
|
|
// 为什么不用 window.open:那样开出来的是 Electron 默认的裸窗口——没有 Node、没有插件 API、
|
|
// 还带着 Eagle 的应用菜单栏(Eagle 只对自己创建的插件窗口调 setMenuBarVisibility(false))。
|
|
// create-plugin-window 正是 Eagle 前端开插件窗口用的通道,manifest 由调用方传进来,
|
|
// 所以我们可以指定 url / 尺寸 / 置顶,拿到的就是"正规的插件窗口"。
|
|
//
|
|
// 两个窗口之间没有 opener 关系,进度用临时目录里的状态文件传:宿主写,弹窗轮询。
|
|
// 弹窗反过来写一个心跳文件,宿主靠它判断窗口是不是被用户关掉了。
|
|
(function () {
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const i18n = window.SpineThumbI18n;
|
|
const t = i18n ? i18n.t : ((key) => key);
|
|
|
|
const STATE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress.json');
|
|
const ALIVE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-alive');
|
|
// 高度只是初值,弹窗加载后会按内容自己调(eagle.window.setSize)。
|
|
const WINDOW_SIZE = { width: 380, height: 160 };
|
|
// 心跳超过这个时间就认为弹窗已经被关掉,下次需要时重新开。
|
|
const ALIVE_WINDOW_MS = 3000;
|
|
// 刚创建完窗口要给它一点加载时间,这段时间内不重复创建。
|
|
const CREATE_COOLDOWN_MS = 4000;
|
|
|
|
let state = { done: 0, total: 0, name: '', finished: false, text: '' };
|
|
let lastCreateAt = 0;
|
|
let ipcRenderer = null;
|
|
|
|
/** 插件目录的绝对路径。装了 eagleplugin:// 协议后 location.href 不是文件路径,只能靠 Eagle 给的。 */
|
|
function pluginPath() {
|
|
if (window.SpinePluginPath) return window.SpinePluginPath;
|
|
if (typeof eagle !== 'undefined' && eagle.plugin && eagle.plugin.path) return eagle.plugin.path;
|
|
return typeof __dirname !== 'undefined' ? __dirname : null;
|
|
}
|
|
|
|
function getIpcRenderer() {
|
|
if (!ipcRenderer) {
|
|
// Eagle 的 preload 把 require('electron') 换成了 {},另起一个 require 才能拿到真模块。
|
|
// createRequire 只接受绝对路径或 file:// URL,所以基准用插件目录拼出来,不能直接用 location.href。
|
|
const base = path.join(pluginPath() || process.cwd(), 'index.html');
|
|
ipcRenderer = require('module').createRequire(base)('electron').ipcRenderer;
|
|
}
|
|
return ipcRenderer;
|
|
}
|
|
|
|
/** 弹窗还开着吗?靠它写的心跳判断,窗口关掉后心跳自然就停了。 */
|
|
function popupAlive() {
|
|
try {
|
|
return Date.now() - Number(fs.readFileSync(ALIVE_FILE, 'utf8')) < ALIVE_WINDOW_MS;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function writeState() {
|
|
try {
|
|
fs.writeFileSync(STATE_FILE, JSON.stringify({ ...state, updatedAt: Date.now() }), 'utf8');
|
|
} catch (error) { /* 写不进去也不影响生成,面板里一样有进度 */ }
|
|
}
|
|
|
|
/** 用 Eagle 的通道创建插件窗口;manifest 覆盖成进度页面的配置。 */
|
|
function createPopup() {
|
|
const plugin = (typeof eagle !== 'undefined' && eagle.plugin) || {};
|
|
const dir = pluginPath();
|
|
let manifest = plugin.manifest;
|
|
if (!manifest && dir) {
|
|
// 正常情况下 eagle.plugin 里就带 manifest;万一还没注入就自己从插件目录读一份。
|
|
manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
|
|
}
|
|
if (!manifest) return Promise.reject(new Error(t('thumb.modulesMissing', { modules: 'manifest' })));
|
|
return getIpcRenderer().invoke('create-plugin-window', {
|
|
plugin: {
|
|
...plugin,
|
|
path: dir,
|
|
manifest: {
|
|
...manifest,
|
|
// 这个窗口的标题栏文字(只影响这扇窗,插件本身的名字不变)。
|
|
name: t('progress.title'),
|
|
main: {
|
|
...manifest.main,
|
|
// 必须关掉 serviceMode,否则 Eagle 会按 show:false 创建这个窗口。
|
|
serviceMode: false,
|
|
url: 'progress.html?lang=' + encodeURIComponent(i18n ? i18n.locale : 'zh_CN'),
|
|
width: WINDOW_SIZE.width,
|
|
height: WINDOW_SIZE.height,
|
|
show: true,
|
|
// 挂在 Eagle 主窗口上:子窗口天然压在父窗口之上,而且跟着 Eagle 一起显示/隐藏。
|
|
// 这里显式写 false,免得主 manifest 哪天又开了 alwaysOnTop 被 ...manifest.main 带进来,
|
|
// 那会让弹窗盖住所有程序,而不是只待在 Eagle 上面。
|
|
alwaysOnTop: false,
|
|
childWindow: true,
|
|
// 保留系统标题栏:最小化/关闭按钮都在上面(manifest.name 决定标题文字)。
|
|
frame: true,
|
|
resizable: false,
|
|
minimizable: true,
|
|
maximizable: false,
|
|
fullscreenable: false,
|
|
},
|
|
},
|
|
},
|
|
args: {},
|
|
});
|
|
}
|
|
|
|
/** 需要时开窗(或复用已开着的);被环境挡住时返回 false,调用方照常继续跑。 */
|
|
function open() {
|
|
try {
|
|
if (popupAlive()) return true;
|
|
if (Date.now() - lastCreateAt < CREATE_COOLDOWN_MS) return false;
|
|
lastCreateAt = Date.now();
|
|
writeState();
|
|
createPopup().catch(() => { /* 开不出来就只在面板里看进度 */ });
|
|
return true;
|
|
} catch (error) {
|
|
// 弹窗只是附加显示,任何问题都不能影响缩略图生成本身。
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function update(next) {
|
|
try {
|
|
state = { ...state, ...next, finished: false };
|
|
writeState();
|
|
} catch (error) { /* 同上:弹窗不能影响生成流程 */ }
|
|
}
|
|
|
|
function finish(text) {
|
|
try {
|
|
state = { ...state, finished: true, text };
|
|
writeState();
|
|
} catch (error) { /* ignore */ }
|
|
}
|
|
|
|
const api = { open, update, finish };
|
|
|
|
if (typeof window !== 'undefined') window.SpineThumbProgress = api;
|
|
else if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
|
|
})();
|