From 3e45d31437cf690febfaaef23b6a7ecbf05f65fc Mon Sep 17 00:00:00 2001 From: JianMiau Date: Thu, 31 Mar 2022 08:59:10 +0800 Subject: [PATCH] [add] first --- README.md | 60 ++ core/FileUtil.js | 136 +++ i18n/en.js | 55 + i18n/zh.js | 55 + lib/node-fetch.js | 1649 ++++++++++++++++++++++++++++++ lib/vue.global.prod.js | 7 + main.js | 174 ++++ package.json | 21 + panel/index.js | 62 ++ scene-obtain.js | 37 + src/common/config-manager.js | 104 ++ src/eazax/browser-util.js | 61 ++ src/eazax/color-util.js | 37 + src/eazax/css/cocos-class.css | 201 ++++ src/eazax/css/cocos-tag.css | 197 ++++ src/eazax/css/eazax-colors.css | 18 + src/eazax/editor-main-kit.js | 53 + src/eazax/editor-main-util.js | 147 +++ src/eazax/editor-renderer-kit.js | 26 + src/eazax/file-util.js | 158 +++ src/eazax/i18n.js | 36 + src/eazax/main-event.js | 81 ++ src/eazax/package-util.js | 47 + src/eazax/renderer-event.js | 80 ++ src/eazax/updater.js | 92 ++ src/eazax/version-util.js | 61 ++ src/eazax/window-util.js | 80 ++ src/main/editor-api.js | 98 ++ src/main/finder.js | 248 +++++ src/main/index.js | 155 +++ src/main/object-util.js | 70 ++ src/main/panel-manager.js | 86 ++ src/main/parser.js | 161 +++ src/main/printer.js | 108 ++ src/renderer/settings/index.css | 61 ++ src/renderer/settings/index.html | 90 ++ src/renderer/settings/index.js | 191 ++++ 37 files changed, 5003 insertions(+) create mode 100644 README.md create mode 100644 core/FileUtil.js create mode 100644 i18n/en.js create mode 100644 i18n/zh.js create mode 100644 lib/node-fetch.js create mode 100644 lib/vue.global.prod.js create mode 100644 main.js create mode 100644 package.json create mode 100644 panel/index.js create mode 100644 scene-obtain.js create mode 100644 src/common/config-manager.js create mode 100644 src/eazax/browser-util.js create mode 100644 src/eazax/color-util.js create mode 100644 src/eazax/css/cocos-class.css create mode 100644 src/eazax/css/cocos-tag.css create mode 100644 src/eazax/css/eazax-colors.css create mode 100644 src/eazax/editor-main-kit.js create mode 100644 src/eazax/editor-main-util.js create mode 100644 src/eazax/editor-renderer-kit.js create mode 100644 src/eazax/file-util.js create mode 100644 src/eazax/i18n.js create mode 100644 src/eazax/main-event.js create mode 100644 src/eazax/package-util.js create mode 100644 src/eazax/renderer-event.js create mode 100644 src/eazax/updater.js create mode 100644 src/eazax/version-util.js create mode 100644 src/eazax/window-util.js create mode 100644 src/main/editor-api.js create mode 100644 src/main/finder.js create mode 100644 src/main/index.js create mode 100644 src/main/object-util.js create mode 100644 src/main/panel-manager.js create mode 100644 src/main/parser.js create mode 100644 src/main/printer.js create mode 100644 src/renderer/settings/index.css create mode 100644 src/renderer/settings/index.html create mode 100644 src/renderer/settings/index.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..6846297 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Cocos Creater OutputComponent + +## 插件 安裝方式 + +1. 安裝在 **項目** 底下 +> 把插件資料夾放到項目的packages(跟assets同層) + +2. 安裝在 **全局** 底下 +> 把插件資料夾放到C:\Users\%USERNAME%\.CocosCreator\packages + +## 插件稍微說明(都是搬過來的資料 XD) + +剩下沒寫的可以到參考資料裡面看看😀 + +1. 定義你的包描述文件:**package.json** +> **name** String - 定義了包的名字,包的名字是全局唯一的,他關係到你今後在官網服務器上登錄時的名字。 +> +> **version** String - 版本號,我們推薦使用semver格式管理你的包版本。 +> +> **description** String(可选) - 一句話描述你的包是做什麼的。 +> +> **author** String(可选) - 擴展包的作者 +> +> **main** String (可选) - 入口程序 +> +> **scene-script** String (可选) - 調用引擎API 和項目腳本 +> +> **main-menu** Object (可选) - 主菜單定義 +> +> **有要使用介面的話:** +> +> **panel** Object (可选) - 定義的面板在package裡的描述 +> +> **注意panel的type有兩種:** +> +> dockable:可停靠面板,打開該面板後,可以通過拖拽面板標籤到編輯器裡,實現擴展面板嵌入到編輯器中。下面我們介紹的面板入口程序都是按照可停靠面板的要求聲明的。 +> +> simple:簡單Web面板,不可停靠到編輯器主窗口,相當於一份通用的HTML前端頁面。詳細情況請見定義簡單面板。 +> +> 在simple-package文件夾下面創建一個panel文件夾,然後在panel文件夾下創建一個index.js或者一個html文件都可以 + +2. 入口程序:**main.js** + +3. 定義介面以及按鈕綁定的方法,和主進程的通信:**index.js** + +3. 可以使用包括全部引擎API 和用戶組件腳本里聲明的方法和屬性:**scene-obtain..js** +> 可以在擴展包中獲取到場景裡的Canvas根節點有多少子節點,當然還可以用來對場景節點進行更多的查詢和操作。 + +## 參考資料 + +* 你的第一個擴展包 +> https://docs.cocos.com/creator/manual/zh/extension/your-first-extension.html + +* CocosCreator拓展编辑器 +> https://blog.csdn.net/qq_34772097/category_9577457.html + +* Cocos Creator Editor 編輯器擴展API記錄 +> https://blog.csdn.net/kingbook928/article/details/108659319 +> https://blog.csdn.net/qq_17209641/article/details/106822296 +> https://forum.cocos.org/t/creator-api/92605 \ No newline at end of file diff --git a/core/FileUtil.js b/core/FileUtil.js new file mode 100644 index 0000000..e36af35 --- /dev/null +++ b/core/FileUtil.js @@ -0,0 +1,136 @@ +let fs = require("fire-fs"); +let path = require("fire-path"); + +let self = module.exports = { + getDirAllFiles(dirPath, result) { + let files = fs.readdirSync(dirPath); + files.forEach((val, index) => { + let fPath = path.join(dirPath, val); + let stats = fs.statSync(fPath); + if (stats.isDirectory()) { + this.getDirAllFiles(fPath, result); + } else if (stats.isFile()) { + result.push(fPath); + } + }); + }, + getFileString(fileList, options) { + let curIndex = 0; + let totalIndex = fileList.length; + let str = {}; + for (let key in fileList) { + let filePath = fileList[key]; + let b = this._isFileExit(filePath); + if (b) { + fs.readFile(filePath, 'utf-8', function (err, data) { + if (!err) { + self._collectString(data, str); + } else { + console.log("error: " + filePath); + } + self._onCollectStep(filePath, ++curIndex, totalIndex, str, options); + }) + } else { + self._onCollectStep(filePath, ++curIndex, totalIndex, str, options); + } + } + }, + _onCollectStep(filePath, cur, total, str, data) { + if (data && data.stepCb) { + data.stepCb(filePath, cur, total); + } + if (cur >= total) { + self._onCollectOver(str, data); + } + }, + _onCollectOver(collectObjArr, data) { + let strArr = []; + let str = ""; + for (let k in collectObjArr) { + str += k; + strArr.push(k); + } + // console.log("一共有" + strArr.length + "个字符, " + strArr); + console.log("一共有" + strArr.length + "个字符"); + if (data && data.compCb) { + data.compCb(str); + } + }, + mkDir(path) { + try { + fs.mkdirSync(path); + } catch (e) { + if (e.code !== 'EEXIST') throw e; + } + }, + isFileExit(file) { + try { + fs.accessSync(file, fs.F_OK); + } catch (e) { + return false; + } + return true; + }, + _collectString(data, collectObject) { + for (let i in data) { + let char = data.charAt(i); + if (collectObject[char]) { + collectObject[char]++; + } else { + collectObject[char] = 1; + } + } + }, + emptyDir(rootFile) { + //删除所有的文件(将所有文件夹置空) + let emptyDir = function (fileUrl) { + let files = fs.readdirSync(fileUrl);//读取该文件夹 + for (let k in files) { + let filePath = path.join(fileUrl, files[k]); + let stats = fs.statSync(filePath); + if (stats.isDirectory()) { + emptyDir(filePath); + } else { + fs.unlinkSync(filePath); + console.log("删除文件:" + filePath); + } + } + }; + //删除所有的空文件夹 + let rmEmptyDir = function (fileUrl) { + let files = fs.readdirSync(fileUrl); + if (files.length > 0) { + for (let k in files) { + let rmDir = path.join(fileUrl, files[k]); + rmEmptyDir(rmDir); + } + if (fileUrl !== rootFile) {// 不删除根目录 + fs.rmdirSync(fileUrl); + console.log('删除空文件夹' + fileUrl); + } + } else { + if (fileUrl !== rootFile) {// 不删除根目录 + fs.rmdirSync(fileUrl); + console.log('删除空文件夹' + fileUrl); + } + } + }; + emptyDir(rootFile); + rmEmptyDir(rootFile); + }, + /* + is_fileType($('#uploadfile').val(), 'doc,pdf,txt,wps,odf,md,png,gif,jpg') + * */ + is_fileType(filename, types) { + types = types.split(','); + let pattern = '\.('; + for (let i = 0; i < types.length; i++) { + if (0 !== i) { + pattern += '|'; + } + pattern += types[i].trim(); + } + pattern += ')$'; + return new RegExp(pattern, 'i').test(filename); + } +} \ No newline at end of file diff --git a/i18n/en.js b/i18n/en.js new file mode 100644 index 0000000..3a0ba29 --- /dev/null +++ b/i18n/en.js @@ -0,0 +1,55 @@ +module.exports = { + 'name': 'References Finder', + 'find': 'Find Current Selected', + 'find-panel': 'Find Panel', + 'settings': 'Settings', + 'check-update': 'Check Update', + // update + 'current-latest': 'Currently the latest version!', + 'has-new-version': 'New version found!', + 'local-version': 'Local version: ', + 'latest-version': 'Latest version: ', + 'git-releases': 'Releases: https://gitee.com/ifaswind/ccc-references-finder/releases', + 'cocos-store': 'Cocos Store: https://store.cocos.com/app/detail/2531', + // main + 'please-select-assets': 'Please select asset(s) in Asset Panel first', + 'invalid-uuid': 'Invalid uuid', + 'not-support-folder': 'Does not support folder', + 'find-asset-refs': 'Find references', + 'no-refs': 'No references found', + 'scene': 'Scene', + 'prefab': 'Prefab', + 'animation': 'Animation', + 'material': 'Material', + 'font': 'Font', + 'node': 'Node', + 'component': 'Component', + 'property': 'Property', + 'result': 'Reference result', + 'node-refs': 'Node References', + 'asset-refs': 'Asset References', + 'asset-info': 'Asset Info', + 'asset-type': 'Type: ', + 'asset-uuid': 'Uuid: ', + 'asset-url': 'Url: ', + 'asset-path': 'Path: ', + // settings + 'none': 'None', + 'select-key': 'Hotkey', + 'select-key-tooltip': 'Chose a hotkey to open the search bar quickly', + 'custom-key': 'Custom', + 'custom-key-placeholder': 'Choose a hotkey above or customize one by yourself', + 'custom-key-tooltip': 'You can also customize your own hotkey', + 'auto-check-update': 'Auto Check Update', + 'auto-check-update-tooltip': 'Check if there is a new version when the extension is loaded', + 'reference': '· Hotkey customization reference: ', + 'accelerator': 'Keyboard Shortcuts', + 'repository': '· Git repository of this extension: ', + 'apply': 'Apply', + 'quote-error': 'Do not use double quotes!', + 'custom-key-error': 'Please specify a hotkey!', + 'print-details': 'Show Details', + 'print-details-tooltip': 'Show more details(node, component, property)', + 'print-folding': 'Fold Result', + 'print-folding-tooltip': 'Fold result in one log', +}; diff --git a/i18n/zh.js b/i18n/zh.js new file mode 100644 index 0000000..c373516 --- /dev/null +++ b/i18n/zh.js @@ -0,0 +1,55 @@ +module.exports = { + 'name': '引用查找器', + 'find': '查找当前选中资源', + 'find-panel': '查找面板', + 'settings': '设置', + 'check-update': '检查更新', + // update + 'current-latest': '当前已是最新版本!', + 'has-new-version': '发现新版本!', + 'local-version': '本地版本:', + 'latest-version': '最新版本:', + 'git-releases': '发行版:https://gitee.com/ifaswind/ccc-references-finder/releases', + 'cocos-store': 'Cocos 商店:https://store.cocos.com/app/detail/2531', + // main + 'please-select-assets': '请先在资源管理器中选择需要查找引用的资源', + 'invalid-uuid': '无效的 uuid', + 'not-support-folder': '暂不支持查找文件夹', + 'find-asset-refs': '查找资源引用', + 'no-refs': '没有找到该资源的引用', + 'scene': '场景', + 'prefab': '预制体', + 'animation': '动画', + 'material': '材质', + 'font': '字体', + 'node': '节点', + 'component': '组件', + 'property': '属性', + 'result': '引用查找结果', + 'node-refs': '节点引用', + 'asset-refs': '资源引用', + 'asset-info': '资源信息', + 'asset-type': 'Type:', + 'asset-uuid': 'Uuid:', + 'asset-url': 'Url:', + 'asset-path': 'Path:', + // settings + 'none': '无', + 'select-key': '快捷键', + 'select-key-tooltip': '选择一个快速打开搜索栏的快捷键', + 'custom-key': '自定义', + 'custom-key-placeholder': '在上方选择一个快捷键或自定义一个快捷键', + 'custom-key-tooltip': '自定义快速打开搜索栏的快捷键', + 'auto-check-update': '自动检查更新', + 'auto-check-update-tooltip': '扩展启动时自动检查是否有新版本', + 'reference': '· 快捷键自定义请参考:', + 'accelerator': '键盘快捷键', + 'repository': '· 本扩展的 Git 仓库:', + 'apply': '应用', + 'quote-error': '请勿使用双引号!', + 'custom-key-error': '请指定一个快捷键!', + 'print-details': '展示详情', + 'print-details-tooltip': '引用查找结果精确到节点、组件和属性', + 'print-folding': '折叠结果', + 'print-folding-tooltip': '引用查找结果将需要手动展开,拯救你的控制台', +}; diff --git a/lib/node-fetch.js b/lib/node-fetch.js new file mode 100644 index 0000000..4b241bf --- /dev/null +++ b/lib/node-fetch.js @@ -0,0 +1,1649 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var Stream = _interopDefault(require('stream')); +var http = _interopDefault(require('http')); +var Url = _interopDefault(require('url')); +var https = _interopDefault(require('https')); +var zlib = _interopDefault(require('zlib')); + +// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js + +// fix for "Readable" isn't a named export issue +const Readable = Stream.Readable; + +const BUFFER = Symbol('buffer'); +const TYPE = Symbol('type'); + +class Blob { + constructor() { + this[TYPE] = ''; + + const blobParts = arguments[0]; + const options = arguments[1]; + + const buffers = []; + let size = 0; + + if (blobParts) { + const a = blobParts; + const length = Number(a.length); + for (let i = 0; i < length; i++) { + const element = a[i]; + let buffer; + if (element instanceof Buffer) { + buffer = element; + } else if (ArrayBuffer.isView(element)) { + buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); + } else if (element instanceof ArrayBuffer) { + buffer = Buffer.from(element); + } else if (element instanceof Blob) { + buffer = element[BUFFER]; + } else { + buffer = Buffer.from(typeof element === 'string' ? element : String(element)); + } + size += buffer.length; + buffers.push(buffer); + } + } + + this[BUFFER] = Buffer.concat(buffers); + + let type = options && options.type !== undefined && String(options.type).toLowerCase(); + if (type && !/[^\u0020-\u007E]/.test(type)) { + this[TYPE] = type; + } + } + get size() { + return this[BUFFER].length; + } + get type() { + return this[TYPE]; + } + text() { + return Promise.resolve(this[BUFFER].toString()); + } + arrayBuffer() { + const buf = this[BUFFER]; + const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + return Promise.resolve(ab); + } + stream() { + const readable = new Readable(); + readable._read = function () {}; + readable.push(this[BUFFER]); + readable.push(null); + return readable; + } + toString() { + return '[object Blob]'; + } + slice() { + const size = this.size; + + const start = arguments[0]; + const end = arguments[1]; + let relativeStart, relativeEnd; + if (start === undefined) { + relativeStart = 0; + } else if (start < 0) { + relativeStart = Math.max(size + start, 0); + } else { + relativeStart = Math.min(start, size); + } + if (end === undefined) { + relativeEnd = size; + } else if (end < 0) { + relativeEnd = Math.max(size + end, 0); + } else { + relativeEnd = Math.min(end, size); + } + const span = Math.max(relativeEnd - relativeStart, 0); + + const buffer = this[BUFFER]; + const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); + const blob = new Blob([], { type: arguments[2] }); + blob[BUFFER] = slicedBuffer; + return blob; + } +} + +Object.defineProperties(Blob.prototype, { + size: { enumerable: true }, + type: { enumerable: true }, + slice: { enumerable: true } +}); + +Object.defineProperty(Blob.prototype, Symbol.toStringTag, { + value: 'Blob', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * fetch-error.js + * + * FetchError interface for operational errors + */ + +/** + * Create FetchError instance + * + * @param String message Error message for human + * @param String type Error type for machine + * @param String systemError For Node.js system error + * @return FetchError + */ +function FetchError(message, type, systemError) { + Error.call(this, message); + + this.message = message; + this.type = type; + + // when err.type is `system`, err.code contains system error code + if (systemError) { + this.code = this.errno = systemError.code; + } + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +FetchError.prototype = Object.create(Error.prototype); +FetchError.prototype.constructor = FetchError; +FetchError.prototype.name = 'FetchError'; + +let convert; +try { + convert = require('encoding').convert; +} catch (e) {} + +const INTERNALS = Symbol('Body internals'); + +// fix an issue where "PassThrough" isn't a named export for node <10 +const PassThrough = Stream.PassThrough; + +/** + * Body mixin + * + * Ref: https://fetch.spec.whatwg.org/#body + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +function Body(body) { + var _this = this; + + var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + _ref$size = _ref.size; + + let size = _ref$size === undefined ? 0 : _ref$size; + var _ref$timeout = _ref.timeout; + let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; + + if (body == null) { + // body is undefined or null + body = null; + } else if (isURLSearchParams(body)) { + // body is a URLSearchParams + body = Buffer.from(body.toString()); + } else if (isBlob(body)) ; else if (Buffer.isBuffer(body)) ; else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { + // body is ArrayBuffer + body = Buffer.from(body); + } else if (ArrayBuffer.isView(body)) { + // body is ArrayBufferView + body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); + } else if (body instanceof Stream) ; else { + // none of the above + // coerce to string then buffer + body = Buffer.from(String(body)); + } + this[INTERNALS] = { + body, + disturbed: false, + error: null + }; + this.size = size; + this.timeout = timeout; + + if (body instanceof Stream) { + body.on('error', function (err) { + const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); + _this[INTERNALS].error = error; + }); + } +} + +Body.prototype = { + get body() { + return this[INTERNALS].body; + }, + + get bodyUsed() { + return this[INTERNALS].disturbed; + }, + + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + arrayBuffer() { + return consumeBody.call(this).then(function (buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + }); + }, + + /** + * Return raw response as Blob + * + * @return Promise + */ + blob() { + let ct = this.headers && this.headers.get('content-type') || ''; + return consumeBody.call(this).then(function (buf) { + return Object.assign( + // Prevent copying + new Blob([], { + type: ct.toLowerCase() + }), { + [BUFFER]: buf + }); + }); + }, + + /** + * Decode response as json + * + * @return Promise + */ + json() { + var _this2 = this; + + return consumeBody.call(this).then(function (buffer) { + try { + return JSON.parse(buffer.toString()); + } catch (err) { + return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); + } + }); + }, + + /** + * Decode response as text + * + * @return Promise + */ + text() { + return consumeBody.call(this).then(function (buffer) { + return buffer.toString(); + }); + }, + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return consumeBody.call(this); + }, + + /** + * Decode response as text, while automatically detecting the encoding and + * trying to decode to UTF-8 (non-spec api) + * + * @return Promise + */ + textConverted() { + var _this3 = this; + + return consumeBody.call(this).then(function (buffer) { + return convertBody(buffer, _this3.headers); + }); + } +}; + +// In browsers, all properties are enumerable. +Object.defineProperties(Body.prototype, { + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true } +}); + +Body.mixIn = function (proto) { + for (const name of Object.getOwnPropertyNames(Body.prototype)) { + // istanbul ignore else: future proof + if (!(name in proto)) { + const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); + Object.defineProperty(proto, name, desc); + } + } +}; + +/** + * Consume and convert an entire Body to a Buffer. + * + * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body + * + * @return Promise + */ +function consumeBody() { + var _this4 = this; + + if (this[INTERNALS].disturbed) { + return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`)); + } + + this[INTERNALS].disturbed = true; + + if (this[INTERNALS].error) { + return Body.Promise.reject(this[INTERNALS].error); + } + + let body = this.body; + + // body is null + if (body === null) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is blob + if (isBlob(body)) { + body = body.stream(); + } + + // body is buffer + if (Buffer.isBuffer(body)) { + return Body.Promise.resolve(body); + } + + // istanbul ignore if: should never happen + if (!(body instanceof Stream)) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is stream + // get ready to actually consume the body + let accum = []; + let accumBytes = 0; + let abort = false; + + return new Body.Promise(function (resolve, reject) { + let resTimeout; + + // allow timeout on slow response body + if (_this4.timeout) { + resTimeout = setTimeout(function () { + abort = true; + reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, 'body-timeout')); + }, _this4.timeout); + } + + // handle stream errors + body.on('error', function (err) { + if (err.name === 'AbortError') { + // if the request was aborted, reject with this Error + abort = true; + reject(err); + } else { + // other errors, such as incorrect content-encoding + reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + + body.on('data', function (chunk) { + if (abort || chunk === null) { + return; + } + + if (_this4.size && accumBytes + chunk.length > _this4.size) { + abort = true; + reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); + return; + } + + accumBytes += chunk.length; + accum.push(chunk); + }); + + body.on('end', function () { + if (abort) { + return; + } + + clearTimeout(resTimeout); + + try { + resolve(Buffer.concat(accum, accumBytes)); + } catch (err) { + // handle streams that have accumulated too much data (issue #414) + reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, 'system', err)); + } + }); + }); +} + +/** + * Detect buffer encoding and convert to target encoding + * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding + * + * @param Buffer buffer Incoming buffer + * @param String encoding Target encoding + * @return String + */ +function convertBody(buffer, headers) { + if (typeof convert !== 'function') { + throw new Error('The package `encoding` must be installed to use the textConverted() function'); + } + + const ct = headers.get('content-type'); + let charset = 'utf-8'; + let res, str; + + // header + if (ct) { + res = /charset=([^;]*)/i.exec(ct); + } + + // no charset in content type, peek at response body for at most 1024 bytes + str = buffer.slice(0, 1024).toString(); + + // html5 + if (!res && str) { + res = / 0 && arguments[0] !== undefined ? arguments[0] : undefined; + + this[MAP] = Object.create(null); + + if (init instanceof Headers) { + const rawHeaders = init.raw(); + const headerNames = Object.keys(rawHeaders); + + for (const headerName of headerNames) { + for (const value of rawHeaders[headerName]) { + this.append(headerName, value); + } + } + + return; + } + + // We don't worry about converting prop to ByteString here as append() + // will handle it. + if (init == null) ; else if (typeof init === 'object') { + const method = init[Symbol.iterator]; + if (method != null) { + if (typeof method !== 'function') { + throw new TypeError('Header pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError('Each header pair must be iterable'); + } + pairs.push(Array.from(pair)); + } + + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError('Each header pair must be a name/value tuple'); + } + this.append(pair[0], pair[1]); + } + } else { + // record + for (const key of Object.keys(init)) { + const value = init[key]; + this.append(key, value); + } + } + } else { + throw new TypeError('Provided initializer must be an object'); + } + } + + /** + * Return combined header value given name + * + * @param String name Header name + * @return Mixed + */ + get(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key === undefined) { + return null; + } + + return this[MAP][key].join(', '); + } + + /** + * Iterate over all headers + * + * @param Function callback Executed for each item with parameters (value, name, thisArg) + * @param Boolean thisArg `this` context for callback function + * @return Void + */ + forEach(callback) { + let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + + let pairs = getHeaders(this); + let i = 0; + while (i < pairs.length) { + var _pairs$i = pairs[i]; + const name = _pairs$i[0], + value = _pairs$i[1]; + + callback.call(thisArg, value, name, this); + pairs = getHeaders(this); + i++; + } + } + + /** + * Overwrite header values given name + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + set(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + this[MAP][key !== undefined ? key : name] = [value]; + } + + /** + * Append a value onto existing header + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + append(name, value) { + name = `${name}`; + value = `${value}`; + validateName(name); + validateValue(value); + const key = find(this[MAP], name); + if (key !== undefined) { + this[MAP][key].push(value); + } else { + this[MAP][name] = [value]; + } + } + + /** + * Check for header name existence + * + * @param String name Header name + * @return Boolean + */ + has(name) { + name = `${name}`; + validateName(name); + return find(this[MAP], name) !== undefined; + } + + /** + * Delete all header values given name + * + * @param String name Header name + * @return Void + */ + delete(name) { + name = `${name}`; + validateName(name); + const key = find(this[MAP], name); + if (key !== undefined) { + delete this[MAP][key]; + } + } + + /** + * Return raw headers (non-spec api) + * + * @return Object + */ + raw() { + return this[MAP]; + } + + /** + * Get an iterator on keys. + * + * @return Iterator + */ + keys() { + return createHeadersIterator(this, 'key'); + } + + /** + * Get an iterator on values. + * + * @return Iterator + */ + values() { + return createHeadersIterator(this, 'value'); + } + + /** + * Get an iterator on entries. + * + * This is the default iterator of the Headers object. + * + * @return Iterator + */ + [Symbol.iterator]() { + return createHeadersIterator(this, 'key+value'); + } +} +Headers.prototype.entries = Headers.prototype[Symbol.iterator]; + +Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + value: 'Headers', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Headers.prototype, { + get: { enumerable: true }, + forEach: { enumerable: true }, + set: { enumerable: true }, + append: { enumerable: true }, + has: { enumerable: true }, + delete: { enumerable: true }, + keys: { enumerable: true }, + values: { enumerable: true }, + entries: { enumerable: true } +}); + +function getHeaders(headers) { + let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value'; + + const keys = Object.keys(headers[MAP]).sort(); + return keys.map(kind === 'key' ? function (k) { + return k.toLowerCase(); + } : kind === 'value' ? function (k) { + return headers[MAP][k].join(', '); + } : function (k) { + return [k.toLowerCase(), headers[MAP][k].join(', ')]; + }); +} + +const INTERNAL = Symbol('internal'); + +function createHeadersIterator(target, kind) { + const iterator = Object.create(HeadersIteratorPrototype); + iterator[INTERNAL] = { + target, + kind, + index: 0 + }; + return iterator; +} + +const HeadersIteratorPrototype = Object.setPrototypeOf({ + next() { + // istanbul ignore if + if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { + throw new TypeError('Value of `this` is not a HeadersIterator'); + } + + var _INTERNAL = this[INTERNAL]; + const target = _INTERNAL.target, + kind = _INTERNAL.kind, + index = _INTERNAL.index; + + const values = getHeaders(target, kind); + const len = values.length; + if (index >= len) { + return { + value: undefined, + done: true + }; + } + + this[INTERNAL].index = index + 1; + + return { + value: values[index], + done: false + }; + } +}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); + +Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { + value: 'HeadersIterator', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * Export the Headers object in a form that Node.js can consume. + * + * @param Headers headers + * @return Object + */ +function exportNodeCompatibleHeaders(headers) { + const obj = Object.assign({ __proto__: null }, headers[MAP]); + + // http.request() only supports string as Host header. This hack makes + // specifying custom Host header possible. + const hostHeaderKey = find(headers[MAP], 'Host'); + if (hostHeaderKey !== undefined) { + obj[hostHeaderKey] = obj[hostHeaderKey][0]; + } + + return obj; +} + +/** + * Create a Headers object from an object of headers, ignoring those that do + * not conform to HTTP grammar productions. + * + * @param Object obj Object of headers + * @return Headers + */ +function createHeadersLenient(obj) { + const headers = new Headers(); + for (const name of Object.keys(obj)) { + if (invalidTokenRegex.test(name)) { + continue; + } + if (Array.isArray(obj[name])) { + for (const val of obj[name]) { + if (invalidHeaderCharRegex.test(val)) { + continue; + } + if (headers[MAP][name] === undefined) { + headers[MAP][name] = [val]; + } else { + headers[MAP][name].push(val); + } + } + } else if (!invalidHeaderCharRegex.test(obj[name])) { + headers[MAP][name] = [obj[name]]; + } + } + return headers; +} + +const INTERNALS$1 = Symbol('Response internals'); + +// fix an issue where "STATUS_CODES" aren't a named export for node <10 +const STATUS_CODES = http.STATUS_CODES; + +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +class Response { + constructor() { + let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + Body.call(this, body, opts); + + const status = opts.status || 200; + const headers = new Headers(opts.headers); + + if (body != null && !headers.has('Content-Type')) { + const contentType = extractContentType(body); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + this[INTERNALS$1] = { + url: opts.url, + status, + statusText: opts.statusText || STATUS_CODES[status], + headers, + counter: opts.counter + }; + } + + get url() { + return this[INTERNALS$1].url || ''; + } + + get status() { + return this[INTERNALS$1].status; + } + + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300; + } + + get redirected() { + return this[INTERNALS$1].counter > 0; + } + + get statusText() { + return this[INTERNALS$1].statusText; + } + + get headers() { + return this[INTERNALS$1].headers; + } + + /** + * Clone this response + * + * @return Response + */ + clone() { + return new Response(clone(this), { + url: this.url, + status: this.status, + statusText: this.statusText, + headers: this.headers, + ok: this.ok, + redirected: this.redirected + }); + } +} + +Body.mixIn(Response.prototype); + +Object.defineProperties(Response.prototype, { + url: { enumerable: true }, + status: { enumerable: true }, + ok: { enumerable: true }, + redirected: { enumerable: true }, + statusText: { enumerable: true }, + headers: { enumerable: true }, + clone: { enumerable: true } +}); + +Object.defineProperty(Response.prototype, Symbol.toStringTag, { + value: 'Response', + writable: false, + enumerable: false, + configurable: true +}); + +const INTERNALS$2 = Symbol('Request internals'); + +// fix an issue where "format", "parse" aren't a named export for node <10 +const parse_url = Url.parse; +const format_url = Url.format; + +const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; + +/** + * Check if a value is an instance of Request. + * + * @param Mixed input + * @return Boolean + */ +function isRequest(input) { + return typeof input === 'object' && typeof input[INTERNALS$2] === 'object'; +} + +function isAbortSignal(signal) { + const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); + return !!(proto && proto.constructor.name === 'AbortSignal'); +} + +/** + * Request class + * + * @param Mixed input Url or Request instance + * @param Object init Custom options + * @return Void + */ +class Request { + constructor(input) { + let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + let parsedURL; + + // normalize input + if (!isRequest(input)) { + if (input && input.href) { + // in order to support Node.js' Url objects; though WHATWG's URL objects + // will fall into this branch also (since their `toString()` will return + // `href` property anyway) + parsedURL = parse_url(input.href); + } else { + // coerce input to a string before attempting to parse + parsedURL = parse_url(`${input}`); + } + input = {}; + } else { + parsedURL = parse_url(input.url); + } + + let method = init.method || input.method || 'GET'; + method = method.toUpperCase(); + + if ((init.body != null || isRequest(input) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { + throw new TypeError('Request with GET/HEAD method cannot have body'); + } + + let inputBody = init.body != null ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; + + Body.call(this, inputBody, { + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); + + const headers = new Headers(init.headers || input.headers || {}); + + if (inputBody != null && !headers.has('Content-Type')) { + const contentType = extractContentType(inputBody); + if (contentType) { + headers.append('Content-Type', contentType); + } + } + + let signal = isRequest(input) ? input.signal : null; + if ('signal' in init) signal = init.signal; + + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError('Expected signal to be an instanceof AbortSignal'); + } + + this[INTERNALS$2] = { + method, + redirect: init.redirect || input.redirect || 'follow', + headers, + parsedURL, + signal + }; + + // node-fetch-only options + this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; + this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + } + + get method() { + return this[INTERNALS$2].method; + } + + get url() { + return format_url(this[INTERNALS$2].parsedURL); + } + + get headers() { + return this[INTERNALS$2].headers; + } + + get redirect() { + return this[INTERNALS$2].redirect; + } + + get signal() { + return this[INTERNALS$2].signal; + } + + /** + * Clone this request + * + * @return Request + */ + clone() { + return new Request(this); + } +} + +Body.mixIn(Request.prototype); + +Object.defineProperty(Request.prototype, Symbol.toStringTag, { + value: 'Request', + writable: false, + enumerable: false, + configurable: true +}); + +Object.defineProperties(Request.prototype, { + method: { enumerable: true }, + url: { enumerable: true }, + headers: { enumerable: true }, + redirect: { enumerable: true }, + clone: { enumerable: true }, + signal: { enumerable: true } +}); + +/** + * Convert a Request to Node.js http request options. + * + * @param Request A Request instance + * @return Object The options object to be passed to http.request + */ +function getNodeRequestOptions(request) { + const parsedURL = request[INTERNALS$2].parsedURL; + const headers = new Headers(request[INTERNALS$2].headers); + + // fetch step 1.3 + if (!headers.has('Accept')) { + headers.set('Accept', '*/*'); + } + + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError('Only absolute URLs are supported'); + } + + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError('Only HTTP(S) protocols are supported'); + } + + if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { + throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); + } + + // HTTP-network-or-cache fetch steps 2.4-2.7 + let contentLengthValue = null; + if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { + contentLengthValue = '0'; + } + if (request.body != null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + contentLengthValue = String(totalBytes); + } + } + if (contentLengthValue) { + headers.set('Content-Length', contentLengthValue); + } + + // HTTP-network-or-cache fetch step 2.11 + if (!headers.has('User-Agent')) { + headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + } + + // HTTP-network-or-cache fetch step 2.15 + if (request.compress && !headers.has('Accept-Encoding')) { + headers.set('Accept-Encoding', 'gzip,deflate'); + } + + let agent = request.agent; + if (typeof agent === 'function') { + agent = agent(parsedURL); + } + + if (!headers.has('Connection') && !agent) { + headers.set('Connection', 'close'); + } + + // HTTP-network fetch step 4.2 + // chunked encoding is handled by Node.js + + return Object.assign({}, parsedURL, { + method: request.method, + headers: exportNodeCompatibleHeaders(headers), + agent + }); +} + +/** + * abort-error.js + * + * AbortError interface for cancelled requests + */ + +/** + * Create AbortError instance + * + * @param String message Error message for human + * @return AbortError + */ +function AbortError(message) { + Error.call(this, message); + + this.type = 'aborted'; + this.message = message; + + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); +} + +AbortError.prototype = Object.create(Error.prototype); +AbortError.prototype.constructor = AbortError; +AbortError.prototype.name = 'AbortError'; + +// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 +const PassThrough$1 = Stream.PassThrough; +const resolve_url = Url.resolve; + +/** + * Fetch function + * + * @param Mixed url Absolute url or Request instance + * @param Object opts Fetch options + * @return Promise + */ +function fetch(url, opts) { + + // allow custom promise + if (!fetch.Promise) { + throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); + } + + Body.Promise = fetch.Promise; + + // wrap http.request into fetch + return new fetch.Promise(function (resolve, reject) { + // build request object + const request = new Request(url, opts); + const options = getNodeRequestOptions(request); + + const send = (options.protocol === 'https:' ? https : http).request; + const signal = request.signal; + + let response = null; + + const abort = function abort() { + let error = new AbortError('The user aborted a request.'); + reject(error); + if (request.body && request.body instanceof Stream.Readable) { + request.body.destroy(error); + } + if (!response || !response.body) return; + response.body.emit('error', error); + }; + + if (signal && signal.aborted) { + abort(); + return; + } + + const abortAndFinalize = function abortAndFinalize() { + abort(); + finalize(); + }; + + // send request + const req = send(options); + let reqTimeout; + + if (signal) { + signal.addEventListener('abort', abortAndFinalize); + } + + function finalize() { + req.abort(); + if (signal) signal.removeEventListener('abort', abortAndFinalize); + clearTimeout(reqTimeout); + } + + if (request.timeout) { + req.once('socket', function (socket) { + reqTimeout = setTimeout(function () { + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + finalize(); + }, request.timeout); + }); + } + + req.on('error', function (err) { + reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + finalize(); + }); + + req.on('response', function (res) { + clearTimeout(reqTimeout); + + const headers = createHeadersLenient(res.headers); + + // HTTP fetch step 5 + if (fetch.isRedirect(res.statusCode)) { + // HTTP fetch step 5.2 + const location = headers.get('Location'); + + // HTTP fetch step 5.3 + const locationURL = location === null ? null : resolve_url(request.url, location); + + // HTTP fetch step 5.5 + switch (request.redirect) { + case 'error': + reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect')); + finalize(); + return; + case 'manual': + // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. + if (locationURL !== null) { + // handle corrupted header + try { + headers.set('Location', locationURL); + } catch (err) { + // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request + reject(err); + } + } + break; + case 'follow': + // HTTP-redirect fetch step 2 + if (locationURL === null) { + break; + } + + // HTTP-redirect fetch step 5 + if (request.counter >= request.follow) { + reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 6 (counter increment) + // Create a new Request object. + const requestOpts = { + headers: new Headers(request.headers), + follow: request.follow, + counter: request.counter + 1, + agent: request.agent, + compress: request.compress, + method: request.method, + body: request.body, + signal: request.signal, + timeout: request.timeout, + size: request.size + }; + + // HTTP-redirect fetch step 9 + if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { + reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); + finalize(); + return; + } + + // HTTP-redirect fetch step 11 + if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { + requestOpts.method = 'GET'; + requestOpts.body = undefined; + requestOpts.headers.delete('content-length'); + } + + // HTTP-redirect fetch step 15 + resolve(fetch(new Request(locationURL, requestOpts))); + finalize(); + return; + } + } + + // prepare response + res.once('end', function () { + if (signal) signal.removeEventListener('abort', abortAndFinalize); + }); + let body = res.pipe(new PassThrough$1()); + + const response_options = { + url: request.url, + status: res.statusCode, + statusText: res.statusMessage, + headers: headers, + size: request.size, + timeout: request.timeout, + counter: request.counter + }; + + // HTTP-network fetch step 12.1.1.3 + const codings = headers.get('Content-Encoding'); + + // HTTP-network fetch step 12.1.1.4: handle content codings + + // in following scenarios we ignore compression support + // 1. compression support is disabled + // 2. HEAD request + // 3. no Content-Encoding header + // 4. no content response (204) + // 5. content not modified response (304) + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + response = new Response(body, response_options); + resolve(response); + return; + } + + // For Node v6+ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + const zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH + }; + + // for gzip + if (codings == 'gzip' || codings == 'x-gzip') { + body = body.pipe(zlib.createGunzip(zlibOptions)); + response = new Response(body, response_options); + resolve(response); + return; + } + + // for deflate + if (codings == 'deflate' || codings == 'x-deflate') { + // handle the infamous raw deflate response from old servers + // a hack for old IIS and Apache servers + const raw = res.pipe(new PassThrough$1()); + raw.once('data', function (chunk) { + // see http://stackoverflow.com/questions/37519828 + if ((chunk[0] & 0x0F) === 0x08) { + body = body.pipe(zlib.createInflate()); + } else { + body = body.pipe(zlib.createInflateRaw()); + } + response = new Response(body, response_options); + resolve(response); + }); + return; + } + + // for br + if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { + body = body.pipe(zlib.createBrotliDecompress()); + response = new Response(body, response_options); + resolve(response); + return; + } + + // otherwise, use response as-is + response = new Response(body, response_options); + resolve(response); + }); + + writeToStream(req, request); + }); +} +/** + * Redirect code matching + * + * @param Number code Status code + * @return Boolean + */ +fetch.isRedirect = function (code) { + return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +}; + +// expose Promise +fetch.Promise = global.Promise; + +module.exports = exports = fetch; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = exports; +exports.Headers = Headers; +exports.Request = Request; +exports.Response = Response; +exports.FetchError = FetchError; diff --git a/lib/vue.global.prod.js b/lib/vue.global.prod.js new file mode 100644 index 0000000..d45ad0c --- /dev/null +++ b/lib/vue.global.prod.js @@ -0,0 +1,7 @@ +// Vue 3.2.4 +// https://cdn.jsdelivr.net/npm/vue@3.2.4/dist/vue.global.prod.js +// converted to es5 (babel) +// minified (babel) +// exported for node modules +function _inherits(e,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(n&&n.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),n&&_setPrototypeOf(e,n)}function _setPrototypeOf(e,n){return _setPrototypeOf=Object.setPrototypeOf||function(e,n){return e.__proto__=n,e},_setPrototypeOf(e,n)}function _createSuper(e){var n=_isNativeReflectConstruct();return function(){var t,o=_getPrototypeOf(e);if(n){var s=_getPrototypeOf(this).constructor;t=Reflect.construct(o,arguments,s)}else t=o.apply(this,arguments);return _possibleConstructorReturn(this,t)}}function _possibleConstructorReturn(e,n){if(n&&("object"===_typeof(n)||"function"==typeof n))return n;if(void 0!==n)throw new TypeError("Derived constructors may only return object or undefined");return _assertThisInitialized(e)}function _assertThisInitialized(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function _isNativeReflectConstruct(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch(n){return!1}}function _getPrototypeOf(e){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},_getPrototypeOf(e)}function _createForOfIteratorHelper(e,n){var t="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!t){if(Array.isArray(e)||(t=_unsupportedIterableToArray(e))||n&&e&&"number"==typeof e.length){t&&(e=t);var s=0,a=function(){};return{s:a,n:function(){return s>=e.length?{done:!0}:{done:!1,value:e[s++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,d=!0,l=!1;return{s:function(){t=t.call(e)},n:function(){var e=t.next();return d=e.done,e},e:function(e){l=!0,r=e},f:function(){try{d||null==t["return"]||t["return"]()}finally{if(l)throw r}}}}function _classCallCheck(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,n){for(var t,o=0;oe.length)&&(n=e.length);for(var t=0,o=Array(n);t=ee?Y(t)||(t.n|=ne,e=!Q(t)):e=!t.has(Ul),e&&(t.add(Ul),Ul.deps.push(t))}function _e(s,e,t,n){var o=Z.get(s);if(o){var a=[];if("clear"===e)a=_toConsumableArray(o.values());else if("length"===t&&Gl(s))o.forEach(function(o,e){("length"==e||e>=n)&&a.push(o)});else switch(void 0!==t&&a.push(o.get(t)),e){case"add":Gl(s)?V(t)&&a.push(o.get("length")):(a.push(o.get(se)),S(s)&&a.push(o.get(re)));break;case"delete":Gl(s)||(a.push(o.get(se)),S(s)&&a.push(o.get(re)));break;case"set":S(s)&&a.push(o.get(se));}if(1===a.length)a[0]&&be(a[0]);else{var r,i=[],d=_createForOfIteratorHelper(a);try{for(d.s();!(r=d.n()).done;){var c=r.value;c&&i.push.apply(i,_toConsumableArray(c))}}catch(e){d.e(e)}finally{d.f()}be(J(i))}}}function be(n){var e,t=_createForOfIteratorHelper(Gl(n)?n:_toConsumableArray(n));try{for(t.s();!(e=t.n()).done;){var o=e.value;(o!==Ul||o.allowRecurse)&&(o.scheduler?o.scheduler():o.run())}}catch(e){t.e(e)}finally{t.f()}}function xe(){var a=!!(0=r.length&&m()):m()},leave:function(e,t){var o=C.key+"";if(e._enterCb&&e._enterCb(!0),k.isUnmounting)return t();v(l,[e]);var a=!1,n=e._leaveCb=function(s){a||(a=!0,t(),v(s?d:u,[e]),e._leaveCb=void 0,y[o]===C&&delete y[o])};y[o]=C,p?(p(e,n),1>=p.length&&n()):n()},clone:function(t){return ln(t,i,k,n)}};return _}function cn(n){if(hn(n))return(n=jo(n)).children=null,n}function an(n){return hn(n)?n.children?n.children[0]:void 0:n}function un(n,e){6&n.shapeFlag&&n.component?un(n.component.subTree,e):128&n.shapeFlag?(n.ssContent.transition=e.clone(n.ssContent),n.ssFallback.transition=e.clone(n.ssFallback)):n.transition=e}function pn(s){for(var e,a=!!(1b||_Pr&&pc.splice(e,1)}(n.update),n.update()}else e.component=s.component,e.el=s.el,n.vnode=e},c=function(d,e,p,u,f,r,s){var i=new ce(function(){if(d.isMounted){var n,t=d.next,o=d.bu,l=d.u,c=d.parent,g=d.vnode,m=t;i.allowRecurse=!1,t?(t.el=g.el,I(d,t,s)):t=g,o&&K(o),(n=t.props&&t.props.onVnodeBeforeUpdate)&&yo(n,c,t,g),i.allowRecurse=!0;var y=Ut(d),v=d.subTree;d.subTree=y,h(v,y,a(v.el),Q(v),d,f,r),t.el=y.el,null===m&&Wt(d,y.el),l&&fo(l,f),(n=t.props&&t.props.onVnodeUpdated)&&fo(function(){return yo(n,c,t,g)},f)}else{var _,b=e,x=b.el,C=b.props,S=d.bm,k=d.m,T=d.parent,w=sn(e);if(i.allowRecurse=!1,S&&K(S),!w&&(_=C&&C.onVnodeBeforeMount)&&yo(_,T,e),i.allowRecurse=!0,x&&P){var N=function(){d.subTree=Ut(d),P(x,d.subTree,d,f,null)};w?e.type.__asyncLoader().then(function(){return!d.isUnmounted&&N()}):N()}else{var E=d.subTree=Ut(d);h(null,E,p,u,d,f,r),e.el=E.el}if(k&&fo(k,f),!w&&(_=C&&C.onVnodeMounted)){var R=e;fo(function(){return yo(_,T,R)},f)}256&e.shapeFlag&&d.a&&fo(d.a,f),d.isMounted=!0,e=p=u=null}},function(){return Sr(d.update)},d.scope),l=d.update=i.run.bind(i);l.id=d.uid,i.allowRecurse=l.allowRecurse=!0,l()},I=function(s,e,t){e.component=s;var n=s.vnode.props;s.vnode=e,s.next=null,function(d,e,t,n){var o=d.props,r=d.attrs,s=d.vnode.patchFlag,i=yt(o),l=_slicedToArray(d.propsOptions,1),p=l[0],c=!1;if(!(n||0a?re(d,o,r,!0,!1,u):S(m,y,n,o,r,s,i,l,u)},L=function(d,e,t,n,o,r,s,i,l){for(var c=0,m=e.length,u=d.length-1,y=m-1;c<=u&&c<=y;){var v=d[c],_=e[c]=l?qo(e[c]):Wo(e[c]);if(!Uo(v,_))break;h(v,_,t,null,o,r,s,i,l),c++}for(;c<=u&&c<=y;){var b=d[u],x=e[y]=l?qo(e[y]):Wo(e[y]);if(!Uo(b,x))break;h(b,x,t,null,o,r,s,i,l),u--,y--}if(c>u){if(c<=y)for(var C=y+1,S=Cy)for(;c<=u;)$(d[c],o,r,!0),c++;else{var k=c,T=c,w=new Map;for(c=T;c<=y;c++){var N=e[c]=l?qo(e[c]):Wo(e[c]);null!=N.key&&w.set(N.key,c)}var E,R=0,I=y-T+1,P=!1,F=0,A=Array(I);for(c=0;c=I){$(V,o,r,!0);continue}var M=void 0;if(null!=V.key)M=w.get(V.key);else for(E=T;E<=y;E++)if(0===A[E-T]&&Uo(V,e[E])){M=E;break}void 0===M?$(V,o,r,!0):(A[M-T]=c+1,M>=F?F=M:P=!0,h(V,e[M],t,null,o,r,s,i,l),R++)}var B=P?function(a){var e,d,p,u,h,f=a.slice(),t=[0],n=a.length;for(e=0;e>1,a[t[h]]E||c!==B[E]?U(O,t,H,2):E--)}}},U=function n(d,e,t,o){var r=4=i.patchFlag||32===i.patchFlag)&&(i=o[a]=qo(o[a]),i.el=r.el),t||vo(r,i))}}function _o(d,e,t,n){var h=n.o.insert,o=n.m,r=4t||arguments.length<=t?void 0:arguments[t],e)if("class"===o)n["class"]!==e["class"]&&(n["class"]=c([n["class"],e["class"]]));else if("style"===o)n.style=r([n.style,e.style]);else if(_(o)){var s=n[o],a=e[o];s!==a&&(n[o]=s?[].concat(s,a):a)}else""!==o&&(n[o]=e[o]);return n}function Yo(n){return n.some(function(n){return!Bo(n)||n.type!==Ao&&(n.type!==Eo||Yo(n.children))})?n:null}function Zo(n){return 4&n.vnode.shapeFlag}function ms(n,e){w(e)?n.render=e:R(e)&&(n.setupState=Nt(e)),Is(n)}function _s(n){Hl=n,Dl=function(n){n.render._rc&&(n.withProxy=new Proxy(n.ctx,ir))}}function Is(n){var e=n.type;if(!n.render){if(Hl&&!e.render){var t=e.template;if(t){var o=n.appContext.config,s=o.isCustomElement,a=o.compilerOptions,r=e.delimiters,i=e.compilerOptions,d=zl(zl({isCustomElement:s,delimiters:r},a),i);e.render=Hl(t,d)}}n.render=e.render||Wl,Dl&&Dl(n)}pr(n),te(),In(n),de(),fr()}function Ps(t){var e;return{get attrs(){return e||(e=function(o){return new Proxy(o.attrs,{get:function(e,t){return ge(o,0,"$attrs"),e[t]}})}(t))},slots:t.slots,emit:t.emit,expose:function(e){t.exposed=e||{}}}}function er(o){if(o.exposed)return o.exposeProxy||(o.exposeProxy=new Proxy(Nt(vt(o.exposed)),{get:function(e,t){return t in e?e[t]:t in rr?rr[t](o):void 0}}))}function nr(n){return w(n)&&n.displayName||n.name}function tr(s,a){var e=!!(2"]):w(e)?["".concat(o,"=fn").concat(e.name?"<".concat(e.name,">"):"")]:(e=yt(e),s?e:["".concat(o,"="),e])}function yr(s,e,t,n){var o;try{o=n?s.apply(void 0,_toConsumableArray(n)):s()}catch(n){br(n,e,t)}return o}function _r(s,a,t,e){if(w(s)){var n=yr(s,a,t,e);return n&&F(n)&&n["catch"](function(n){br(n,a,t)}),n}for(var o=[],r=0;r>>1;vc(pc[a])=t.attached-1)&&_r(function(n,e){if(Gl(e)){var t=n.stopImmediatePropagation;return n.stopImmediatePropagation=function(){t.call(n),n._stopped=!0},e.map(function(n){return function(e){return!e._stopped&&n(e)}})}return e}(n,t.value),s,5,[n])};return e.value=o,e.attached=function(){return xc||(Cc.then(ws),xc=bs())}(),e}(n,o),c):s&&(!function(s,e,t,n){s.removeEventListener(e,t,n)}(a,l,s,c),r[e]=void 0)}}function Ns(s,a){var t=fn(s),e=/*#__PURE__*/function(e){function n(s){return _classCallCheck(this,n),o.call(this,t,s,a)}_inherits(n,e);var o=_createSuper(n);return n}(Fs);return e.def=t,e}function Rs(n,o){if(128&n.shapeFlag){var t=n.suspense;n=t.activeBranch,t.pendingBranch&&!t.isHydrating&&t.effects.push(function(){Rs(t.activeBranch,o)})}for(;n.component;)n=n.component.subTree;if(1&n.shapeFlag&&n.el)Ms(n.el,o);else if(n.type===Eo)n.children.forEach(function(n){return Rs(n,o)});else if(n.type===Mo)for(var s=n,a=s.el,r=s.anchor;a&&(Ms(a,o),a!==r);)a=a.nextSibling}function Ms(n,e){if(1===n.nodeType){var t=n.style;for(var o in e)t.setProperty("--".concat(o),e[o])}}function Os(C){var e={};for(var t in C)t in Tc||(e[t]=C[t]);if(!1===C.css)return e;var I=C.name,P=void 0===I?"v":I,F=C.type,n=C.duration,o=C.enterFromClass,A=void 0===o?"".concat(P,"-enter-from"):o,s=C.enterActiveClass,r=void 0===s?"".concat(P,"-enter-active"):s,i=C.enterToClass,V=void 0===i?"".concat(P,"-enter-to"):i,l=C.appearFromClass,M=void 0===l?A:l,c=C.appearActiveClass,B=void 0===c?r:c,a=C.appearToClass,L=void 0===a?V:a,u=C.leaveFromClass,O=void 0===u?"".concat(P,"-leave-from"):u,p=C.leaveActiveClass,U=void 0===p?"".concat(P,"-leave-active"):p,f=C.leaveToClass,H=void 0===f?"".concat(P,"-leave-to"):f,d=function(n){if(null==n)return null;if(R(n))return[Hs(n.enter),Hs(n.leave)];{var e=Hs(n);return[e,e]}}(n),h=d&&d[0],m=d&&d[1],g=e.onBeforeEnter,v=e.onEnter,y=e.onEnterCancelled,b=e.onLeave,_=e.onLeaveCancelled,S=e.onBeforeAppear,D=void 0===S?g:S,x=e.onAppear,j=void 0===x?v:x,w=e.onAppearCancelled,$=void 0===w?y:w,k=function(o,e,t){Ws(o,e?L:V),Ws(o,e?B:r),t&&t()},T=function(n,e){Ws(n,H),Ws(n,U),e&&e()},N=function(o){return function(e,t){var n=o?j:v,s=function(){return k(e,o,t)};Ls(n,[e,s]),zs(function(){Ws(e,o?M:A),Ds(e,o?L:V),js(n)||Ks(e,F,h,s)})}};return zl(e,{onBeforeEnter:function(n){Ls(g,[n]),Ds(n,A),Ds(n,r)},onBeforeAppear:function(n){Ls(D,[n]),Ds(n,M),Ds(n,B)},onEnter:N(!1),onAppear:N(!0),onLeave:function(o,e){var t=function(){return T(o,e)};Ds(o,O),Qs(),Ds(o,U),zs(function(){Ws(o,O),Ds(o,H),js(b)||Ks(o,F,m,t)}),Ls(b,[o,t])},onEnterCancelled:function(n){k(n,!1),Ls(y,[n])},onAppearCancelled:function(n){k(n,!0),Ls($,[n])},onLeaveCancelled:function(n){T(n),Ls(_,[n])}})}function Hs(n){return z(n)}function Ds(n,e){e.split(/\s+/).forEach(function(e){return e&&n.classList.add(e)}),(n._vtc||(n._vtc=new Set)).add(e)}function Ws(o,e){e.split(/\s+/).forEach(function(e){return e&&o.classList.remove(e)});var t=o._vtc;t&&(t["delete"](e),t.size||(o._vtc=void 0))}function zs(n){requestAnimationFrame(function(){requestAnimationFrame(n)})}function Ks(d,e,t,n){var o=d._endId=++Us,r=function(){o===d._endId&&n()};if(t)return setTimeout(r,t);var s=qs(d,e),h=s.type,i=s.timeout,l=s.propCount;if(!h)return n();var c=h+"end",a=0,g=function(){d.removeEventListener(c,p),r()},p=function(e){e.target===d&&++a>=l&&g()};setTimeout(function(){ac?"transition":"animation":null,g=a?"transition"===a?r.length:l.length:0),{type:a,timeout:h,propCount:g,hasTransform:"transition"===a&&/\b(transform|all)(,|$)/.test(t.transitionProperty)}}function Js(o,n){for(;o.length"===s[2]){Pd(a,3);continue}if(/[a-z]/i.test(s[2])){xd(a,1,n);continue}d=_d(a)}if(d||(d=Td(a,e)),Gl(d))for(var l=0;l/.exec(s.source);if(t){e=s.source.slice(4,t.index);for(var o=s.source.slice(0,t.index),r=1,i=0;-1!==(i=o.indexOf(" + 1. 填入要檢查的資料夾(從assets開始算)
+ 2. 執行
+ PS. 目前只有檢查png、jpg、anim、fnt + +
+
+ 要檢查的資料夾 db://asstes/ + + +
+
+
+
+ 執行 +
+ `, + + $: { + /** filepath */ + filepath: "#filepath", + /** 生成按鈕 */ + run: "#run", + }, + + ready() { + // Editor.Ipc.sendToMain("sourcecheck:panel-load-finish"); + this.onClickRun(); + + }, + /** 保存按鈕點擊事件 */ + onClickRun() { + this.$run.addEventListener("confirm", () => { + if (!this.$filepath._value) { + Editor.error("請輸入要檢查的資料夾"); + return; + } + Editor.Ipc.sendToMain("sourcecheck:run-click", this.$filepath); + }); + }, + messages: { + // "setDefault": function (event, ...agrs) { + // if (event.reply) { + // //if no error, the first argument should be null + // event.reply(null, "Fine, thank you!"); + // } + // } + } +}); \ No newline at end of file diff --git a/scene-obtain.js b/scene-obtain.js new file mode 100644 index 0000000..76b0b4c --- /dev/null +++ b/scene-obtain.js @@ -0,0 +1,37 @@ +"use strict"; +let fs = require("fs"); +let path = require("path"); + +module.exports = { + /** + * 獲取場景節點下的配置信息 + * @param event event + * @param data 這邊把panel的Node帶進來 + */ + "getFsPath": function (event, request) { + let self = this; + var response = null; + var args = request.args; + try { + let url = `db://assets/${args[0]._value}`; + url = url.replace(/\\/g, "/"); + // let url = `db://assets/=SceneLobby`; + // Editor.log(`url: ${url}`); + let fsPath = Editor.remote.assetdb.urlToFspath(url); + // Editor.log(`fsPath: ${fsPath}`); + + // this.fileSearch(fsPath); + + let res = { + url: url, + fsPath, fsPath + } + response = JSON.stringify(res); + } catch (error) { + Editor.log(`getFsPath error: ${error}`); + } + if (event.reply) { + event.reply(null, response); + } + }, +}; \ No newline at end of file diff --git a/src/common/config-manager.js b/src/common/config-manager.js new file mode 100644 index 0000000..68f0ac3 --- /dev/null +++ b/src/common/config-manager.js @@ -0,0 +1,104 @@ +const Path = require('path'); +const Fs = require('fs'); +const PackageUtil = require('../eazax/package-util'); + +/** 配置文件路径 */ +const CONFIG_PATH = Path.join(__dirname, '../../config.json'); + +/** package.json 的路径 */ +const PACKAGE_PATH = Path.join(__dirname, '../../package.json'); + +/** 包名 */ +const PACKAGE_NAME = PackageUtil.name; + +/** 快捷键行为 */ +const ACTION_NAME = 'find'; + +/** package.json 中的菜单项 key */ +const MENU_ITEM_KEY = `i18n:MAIN_MENU.package.title/i18n:${PACKAGE_NAME}.name/i18n:${PACKAGE_NAME}.${ACTION_NAME}`; + +/** + * 配置管理器 + */ +const ConfigManager = { + + /** + * 默认配置 + */ + get defaultConfig() { + return { + version: '1.1', + printDetails: true, + printFolding: true, + autoCheckUpdate: true, + }; + }, + + /** + * 读取配置 + */ + get() { + // 配置 + const config = ConfigManager.defaultConfig; + if (Fs.existsSync(CONFIG_PATH)) { + const localConfig = JSON.parse(Fs.readFileSync(CONFIG_PATH)); + for (const key in config) { + if (localConfig[key] !== undefined) { + config[key] = localConfig[key]; + } + } + } + + // // 快捷键 + // config.hotkey = ConfigManager.getAccelerator(); + + // Done + return config; + }, + + /** + * 保存配置 + * @param {*} value 配置 + */ + set(value) { + // 配置 + const config = ConfigManager.defaultConfig; + for (const key in config) { + if (value[key] !== undefined) { + config[key] = value[key]; + } + } + Fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); + + // 快捷键 + ConfigManager.setAccelerator(value.hotkey); + }, + + /** + * 获取快捷键 + * @returns {string} + */ + getAccelerator() { + const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)), + item = package['main-menu'][MENU_ITEM_KEY]; + return item['accelerator'] || ''; + }, + + /** + * 设置快捷键 + * @param {string} value + */ + setAccelerator(value) { + const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)), + item = package['main-menu'][MENU_ITEM_KEY]; + if (value != undefined && value !== '') { + item['accelerator'] = value; + } else { + delete item['accelerator']; + } + Fs.writeFileSync(PACKAGE_PATH, JSON.stringify(package, null, 2)); + }, + +}; + +module.exports = ConfigManager; diff --git a/src/eazax/browser-util.js b/src/eazax/browser-util.js new file mode 100644 index 0000000..f3af1a0 --- /dev/null +++ b/src/eazax/browser-util.js @@ -0,0 +1,61 @@ +/** + * 浏览器工具 + * @author 陈皮皮 (ifaswind) + * @version 20210729 + */ +const BrowserUtil = { + + /** + * 获取当前 Url 中的参数 + * @param {string} key 键 + * @returns {string} + */ + getUrlParam(key) { + if (!window || !window.location) { + return null; + } + const query = window.location.search.replace('?', ''); + if (query === '') { + return null; + } + const substrings = query.split('&'); + for (let i = 0; i < substrings.length; i++) { + const keyValue = substrings[i].split('='); + if (decodeURIComponent(keyValue[0]) === key) { + return decodeURIComponent(keyValue[1]); + } + } + return null; + }, + + /** + * 获取 Cookie 值 + * @param {string} key 键 + * @returns {string} + */ + getCookie(key) { + const regExp = new RegExp(`(^| )${key}=([^;]*)(;|$)`), + values = document.cookie.match(regExp); + if (values !== null) { + return values[2]; + } + return null; + }, + + /** + * 设置 Cookie + * @param {string} key 键 + * @param {string | number | boolean} value 值 + * @param {string} expires 过期时间(GMT) + */ + setCookie(key, value, expires) { + let keyValues = `${key}=${encodeURIComponent(value)};`; + if (expires) { + keyValues += `expires=${expires};`; + } + document.cookie = keyValues; + }, + +}; + +module.exports = BrowserUtil; diff --git a/src/eazax/color-util.js b/src/eazax/color-util.js new file mode 100644 index 0000000..ddd4f98 --- /dev/null +++ b/src/eazax/color-util.js @@ -0,0 +1,37 @@ +/** + * 颜色工具 + * @author 陈皮皮 (ifaswind) + * @version 20210725 + */ +const ColorUtil = { + + /** + * 将十六进制颜色值转为 RGB 格式 + * @param {string} hex + * @returns {{ r: number, g: number, b: number }} + */ + hexToRGB(hex) { + // 是否为 HEX 格式 + const regExp = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; + if (!regExp.test(hex)) { + return null; + } + // 四位 + if (hex.length === 4) { + const r = hex.slice(1, 2), + g = hex.slice(2, 3), + b = hex.slice(3, 4); + hex = `#${r}${r}${g}${g}${b}${b}`; + } + // 转换进制 + const rgb = { + r: parseInt(`0x${hex.slice(1, 3)}`), + g: parseInt(`0x${hex.slice(3, 5)}`), + b: parseInt(`0x${hex.slice(5, 7)}`), + } + return rgb; + }, + +}; + +module.exports = ColorUtil; diff --git a/src/eazax/css/cocos-class.css b/src/eazax/css/cocos-class.css new file mode 100644 index 0000000..4ea03b1 --- /dev/null +++ b/src/eazax/css/cocos-class.css @@ -0,0 +1,201 @@ +/* + Cocos Creator 风格样式 + 版本: 20210911 + 作者: 陈皮皮 (ifaswind) + 主页: https://gitee.com/ifaswind + 公众号: 菜鸟小栈 +*/ + +/* 属性容器 */ +.properties { + width: 100%; + border: 1px solid #666; + border-radius: 3px; + padding: 5px; + box-sizing: border-box; + outline: 0; + display: flex; + flex-direction: column; + overflow: auto; +} + +.properties > * { + margin: 2px 0; +} + +.properties:first-child { + margin-top: 0; +} + +.properties:last-child { + margin-bottom: 0; +} + +/* 属性 */ +.property { + width: 100%; + min-height: 23px; + box-sizing: border-box; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; +} + +/* 属性标签 */ +.property > .label { + width: 38%; + min-width: 70px; + position: relative; + margin-left: 5px; + line-height: 23px; + font-size: 12px; + white-space: nowrap; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + flex-shrink: 0; + align-items: baseline; + justify-content: flex-start; +} + +/* 属性标签文本:虚指 */ +.property:hover > .label > .text { + color: #09f; +} + +/* 属性标签文本:聚焦内部 */ +.property:focus-within > .label > .text { + color: #fd942b; +} + +/* 属性标签内容:聚焦内部 */ +.property:focus-within > .content > * { + border-color: #fd942b; +} + +/* tooltip */ +.tooltip { + background-color: #333333; + padding: 5px 8px; + border: 1px solid #646464; + border-radius: 4px; + position: absolute; + top: -38px; + left: -5px; + visibility: hidden; + text-align: center; + z-index: 2; +} + +/* tooltip 三角形 */ +.tooltip::before, +.tooltip::after { + content: ''; + display: block; + width: 0; + height: 0; + border: 6px solid transparent; + position: absolute; + left: 10px; + transform: rotate(-90deg); +} + +/* tooltip 三角形 */ +.tooltip::before { + border-right: 6px solid #333333; + top: 100%; +} + +/* tooltip 三角形边框 */ +.tooltip::after { + border-right: 6px solid #646464; + top: calc(100% + 1px); + z-index: -1; +} + +/* 前一个元素虚指时的 tooltip */ +*:hover + .tooltip { + visibility: visible; +} + +/* 属性内容 */ +.property > .content { + display: flex; + flex: 1; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; +} + +.property > .content > * { + width: auto; + min-width: 20px; + height: 21px; + flex: 1; +} + +.property > .content > *:focus { + border-color: #fd942b; +} + +/* 提示 */ +.tip { + width: 100%; + min-height: 45px; + background: #333; + border: 1px solid #666; + border-radius: 3px; + padding: 12px 8px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: left; + color: #bdbdbd; + line-height: 17px; + font-size: 13px; + white-space: pre-line; +} + +.tip > * { + display: inline-block; +} + +/* *::滚动条 */ +*::-webkit-scrollbar { + width: 11px; +} + +/* *::滚动条-按钮 */ +*::-webkit-scrollbar-button { + display: none; +} + +/* *::滚动条-横竖交汇处 */ +*::-webkit-scrollbar-corner { + display: none; +} + +/* *::滚动条-轨道 */ +*::-webkit-scrollbar-track { + /* background: rgba(0, 0, 0, 0.5); */ + background: none !important; + background-clip: content-box; + border: 5px solid transparent; +} + +/* *::滚动条-滑块 */ +*::-webkit-scrollbar-thumb { + background: #7d7d7d; + background-clip: content-box; + border: 4px solid transparent; + border-radius: 6px; +} + +/* *::滚动条-滑块:虚指 */ +*::-webkit-scrollbar-thumb:hover { + background-color: #fd942b; + border: 3px solid transparent; +} diff --git a/src/eazax/css/cocos-tag.css b/src/eazax/css/cocos-tag.css new file mode 100644 index 0000000..ed85581 --- /dev/null +++ b/src/eazax/css/cocos-tag.css @@ -0,0 +1,197 @@ +/* + Cocos Creator 风格标签 (橙黑) + 版本: 20210725 + 作者: 陈皮皮 (ifaswind) + 主页: https://gitee.com/ifaswind + 公众号: 菜鸟小栈 +*/ + +/* 下拉选择器 */ +select { + background-color: #262626; + outline: none; + box-sizing: border-box; + border: 1px solid #171717; + border-radius: 100px; + padding: 0 8px; + font-size: 12px; + color: #bdbdbd; + cursor: pointer; + /* 替换默认的箭头 */ + appearance: none; + -webkit-appearance: none; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAA+ElEQVRYR+2VXw6CMAzGV3YUjSZ6CsI4l3gutnALH/xzEB8spokSQphbiwkv5RHafb9+awuYlR9YWd8ogDqgDqgDPx3w3h8A4FlV1UOysLz3O2stlmV5j+VHAbqu2yLijRIR8VzXdcOBaNu2KYriRDl93x+dc5e5/ChACGFjjBnIORBj8Q/A3jl3ZQFQcAiBKhgqz4GYiqdyklPAgeCKU5FJgFwnJOLZACkIqTgLgILnhOj9t9slE5N1BePunUKMv6Uajj0Fsbmfg5CIs68g5oRUfBEAJdOqBYCXdFUvBuCsZva/4B+H55zBnoKcQzkxCqAOqAOrO/AGwnWWIa30xvoAAAAASUVORK5CYII=); + background-size: 16px; + background-repeat: no-repeat; + background-position: right 3px center; +} + +/* 下拉选择器:虚指 */ +select:hover { + border-color: #888888; +} + +/* 输入框,文本区域 */ +input, +textarea { + background-color: #262626; + box-sizing: border-box; + padding: 0 5px; + border: 1px solid #171717; + border-radius: 3px; + color: #fd942b; + font-size: 12px; + outline: none; +} + +/* 文本区域 */ +textarea { + min-height: 40px; + resize: vertical; +} + +/* 输入框,文本区域::占位符 */ +input::placeholder, +textarea::placeholder { + font-size: 12px; + font-style: normal; +} + +/* 数字输入框 */ +input[type='number'] { + width: 50px !important; +} + +/* 数字输入框::增减按钮 */ +input[type='number']::-webkit-outer-spin-button, +input[type='number']::-webkit-inner-spin-button { + /* appearance: none; */ + /* -webkit-appearance: none; */ + /* margin: 0; */ + margin-right: -2px; +} + +/* 复选框 */ +input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + width: 16px !important; + height: 16px !important; + min-width: 16px !important; + background-image: none; + background-color: #262626; + border: 1px solid #171717; + border-radius: 3px; + padding-left: 0; + position: relative !important; + flex: 0 !important; + margin: 0; + color: #fd942b; + outline: none; + cursor: pointer; +} + +/* 复选框:勾选 */ +input[type='checkbox']:checked::after { + width: 12px; + height: 12px; + display: inline-block; + content: ''; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABKUlEQVRYR+2W2w2DMAxF41ZdBnWzqjMQZkBdjWUqkiqUgCHOO4Ef+EECwTlO7AvATj7gZD67BA5bgW/fcLXdj/cwnfVxiMD4aVomYQILKTssUV0Aw3XVWKKqgFr2G0BLTZqWqCbggq8NIHkVgSD43A/FBag9J8MOJL+/hq6oQGjlbIYrMUNAVSAEg/28+iI7BW4IuObVJZAK3wj45tUmkANfBFyNs08uLBIKd70DgroWNY0WKAGfVmBUHwlLWuFqcRWl4OsWREioh2zxuukTYtWoPlrGcOyfnIEkc9s3gsb9QLg5hiUkIuB0EOVIRMJJAXUxaTsS4FaBaIlEuFMgWCID7hXwSmTCgwSsEgXgwQJ/CZSYheBRAlpCEP/20UGFHij6R5Qicgn8AIQ23JRIyuB1AAAAAElFTkSuQmCC); + background-size: 12px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* 输入框:虚指 */ +input:hover { + border-color: #888888; +} + +/* 滑动条 */ +input[type='range'] { + appearance: none; + -webkit-appearance: none; + height: 4px !important; + background-color: #262626; + border: 1px solid #171717; + padding-left: 0; + padding-right: 0; +} + +/* 滑动条:虚指|聚焦 */ +input[type='range']:hover, +input[type='range']:focus, +*:focus-within input[type='range'] { + border-color: #171717 !important; +} + +/* 滑动条::把手 */ +input[type='range']::-webkit-slider-thumb { + appearance: none; + -webkit-appearance: none; + width: 12px; + height: 12px; + top: 2px; + background: #333; + box-sizing: border-box; + border: 2px solid #949494; + box-shadow: 0 1px 3px 1px #000 inset, 0 1px 1px 0 rgba(0, 0, 0, 0.9); + border-radius: 100%; +} + +/* 滑动条::把手:虚指 */ +input[type='range']::-webkit-slider-thumb:hover { + border-color: #bcbcbc; + cursor: pointer; +} + +/* 滑动条::把手:激活 */ +input[type='range']::-webkit-slider-thumb:active, +*:focus-within > input[type='range']::-webkit-slider-thumb { + color: #bdbdbd; + border-color: #fd942b !important; + cursor: ew-resize; +} + +/* 取色器 */ +input[type='color'] { + width: 16px; + height: 16px; + box-sizing: border-box; + border-radius: 1px; + padding: 0; + cursor: pointer; +} + +/* 取色器::色板容器 */ +input[type='color']::-webkit-color-swatch-wrapper { + padding: 0; +} + +/* 取色器::色板 */ +input[type='color']::-webkit-color-swatch { + border: none; +} + +/* 超链接 */ +a { + color: #fd942b; + text-decoration: none; +} + +/* 超链接:虚指 */ +a:hover { + text-decoration: underline; +} + +/* 分割线 */ +hr { + width: 100%; + height: 1px; + background-color: #666; + border: none; + margin: 10px 0 !important; +} diff --git a/src/eazax/css/eazax-colors.css b/src/eazax/css/eazax-colors.css new file mode 100644 index 0000000..2e1a17e --- /dev/null +++ b/src/eazax/css/eazax-colors.css @@ -0,0 +1,18 @@ +:root { + /* 背景颜色 */ + --eazax-bg-color: #454545; + /* 主颜色 */ + --eazax-main-color: #262626; + /* 强调色 */ + --eazax-accent-color: #2e88fb; + /* 聚焦色 */ + --eazax-focus-color: #fd942b; + /* 边框调色 */ + --eazax-border-color: #171717; + /* 边框虚指调色 */ + --eazax-border-hover-color: #888888; + /* 文本颜色 */ + --eazax-font-color: #bdbdbd; + /* 内容颜色 */ + --eazax-content-color: #fd942b; +} diff --git a/src/eazax/editor-main-kit.js b/src/eazax/editor-main-kit.js new file mode 100644 index 0000000..5c09c9d --- /dev/null +++ b/src/eazax/editor-main-kit.js @@ -0,0 +1,53 @@ +const MainEvent = require('./main-event'); +const { print, checkUpdate } = require('./editor-main-util'); + +/** + * (渲染进程)检查更新回调 + * @param {Electron.IpcMainEvent} event + * @param {boolean} logWhatever 无论有无更新都打印提示 + */ +function onCheckUpdateEvent(event, logWhatever) { + checkUpdate(logWhatever); +} + +/** + * (渲染进程)打印事件回调 + * @param {Electron.IpcMainEvent} event + * @param {'log' | 'info' | 'warn' | 'error' | any} type + * @param {any[]?} args + */ +function onPrintEvent(event, type) { + // print(type, ...args); + const args = [type]; + for (let i = 2, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + print.apply(null, args); +} + +/** + * 编辑器主进程套件 (依赖 Cocos Creator 编辑器) + * @author 陈皮皮 (ifaswind) + * @version 20210818 + */ +const EditorMainKit = { + + /** + * 注册 + */ + register() { + MainEvent.on('check-update', onCheckUpdateEvent); + MainEvent.on('print', onPrintEvent); + }, + + /** + * 取消注册 + */ + unregister() { + MainEvent.removeListener('check-update', onCheckUpdateEvent); + MainEvent.removeListener('print', onPrintEvent); + }, + +}; + +module.exports = EditorMainKit; diff --git a/src/eazax/editor-main-util.js b/src/eazax/editor-main-util.js new file mode 100644 index 0000000..05b920b --- /dev/null +++ b/src/eazax/editor-main-util.js @@ -0,0 +1,147 @@ +const I18n = require('./i18n'); +const PackageUtil = require('./package-util'); +const Updater = require('./updater'); + +/** 编辑器语言 */ +const LANG = Editor.lang || Editor.I18n.getLanguage(); + +/** 包名 */ +const PACKAGE_NAME = PackageUtil.name; + +/** 扩展名称 */ +const EXTENSION_NAME = I18n.get(LANG, 'name'); + +/** + * 编辑器主进程工具 (依赖 Cocos Creator 编辑器) + * @author 陈皮皮 (ifaswind) + * @version 20210929 + */ +const EditorMainUtil = { + + /** + * 语言 + */ + get language() { + return LANG; + }, + + /** + * i18n + * @param {string} key 关键词 + * @returns {string} + */ + translate(key) { + return I18n.get(LANG, key); + }, + + /** + * 打印信息到控制台(带标题) + * @param {'log' | 'info' | 'warn' | 'error' | any} type + * @param {any[]?} args + */ + print(type) { + const args = [`[${EXTENSION_NAME}]`]; + for (let i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + const object = Editor.log ? Editor : console; + switch (type) { + case 'log': { + object.log.apply(object, args); + break; + } + case 'info': { + object.info.apply(object, args); + break; + } + case 'warn': { + object.warn.apply(object, args); + break; + } + case 'error': { + object.error.apply(object, args); + break; + } + default: { + args.splice(1, 0, type); + object.log.apply(object, args); + } + } + }, + + /** + * 打印信息到控制台(不带标题) + * @param {'log' | 'info' | 'warn' | 'error' | any} type + * @param {any[]?} args + */ + pureWithoutTitle(type) { + const args = []; + for (let i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + const object = Editor.log ? Editor : console; + switch (type) { + case 'log': { + object.log.apply(object, args); + break; + } + case 'info': { + object.info.apply(object, args); + break; + } + case 'warn': { + object.warn.apply(object, args); + break; + } + case 'error': { + object.error.apply(object, args); + break; + } + default: { + args.splice(1, 0, type); + object.log.apply(object, args); + } + } + }, + + /** + * 检查更新 + * @param {boolean} logWhatever 无论有无更新都打印提示 + */ + async checkUpdate(logWhatever) { + // 编辑器本次启动是否已经检查过了 + if (!logWhatever && (Editor[PACKAGE_NAME] && Editor[PACKAGE_NAME].hasCheckUpdate)) { + return; + } + Editor[PACKAGE_NAME] = { hasCheckUpdate: true }; + // 是否有新版本 + const hasNewVersion = await Updater.check(); + // 打印到控制台 + const { print, translate } = EditorMainUtil; + const localVersion = Updater.getLocalVersion(); + if (hasNewVersion) { + const remoteVersion = await Updater.getRemoteVersion(); + print('info', translate('has-new-version')); + print('info', `${translate('local-version')}${localVersion}`); + print('info', `${translate('latest-version')}${remoteVersion}`); + print('info', translate('git-releases')); + print('info', translate('cocos-store')); + } else if (logWhatever) { + print('info', translate('current-latest')); + print('info', `${translate('local-version')}${localVersion}`); + } + }, + + /** + * (3.x)重新加载扩展 + */ + async reload() { + const path = await Editor.Package.getPath(PACKAGE_NAME); + await Editor.Package.unregister(path); + await Editor.Package.register(path); + await Editor.Package.enable(path); + }, + +}; + +module.exports = EditorMainUtil; diff --git a/src/eazax/editor-renderer-kit.js b/src/eazax/editor-renderer-kit.js new file mode 100644 index 0000000..238bfae --- /dev/null +++ b/src/eazax/editor-renderer-kit.js @@ -0,0 +1,26 @@ +const RendererEvent = require("./renderer-event"); + +/** + * 编辑器渲染进程套件 (依赖 Cocos Creator 编辑器) + * @author 陈皮皮 (ifaswind) + * @version 20210818 + */ +const EditorRendererKit = { + + /** + * 打印信息到 Creator 编辑器控制台 + * @param {'log' | 'info' | 'warn' | 'error' | any} type + * @param {any[]?} args + */ + print(type) { + // return RendererEvent.send('print', type, ...args); + const args = ['print', type]; + for (let i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return RendererEvent.send.apply(RendererEvent, args); + }, + +}; + +module.exports = EditorRendererKit; diff --git a/src/eazax/file-util.js b/src/eazax/file-util.js new file mode 100644 index 0000000..9743b5b --- /dev/null +++ b/src/eazax/file-util.js @@ -0,0 +1,158 @@ +const Fs = require('fs'); +const Path = require('path'); +const { promisify } = require('util'); + +/** + * 文件工具 (Promise 化) + * @author 陈皮皮 (ifaswind) + * @version 20210818 + */ +const FileUtil = { + + /** + * 获取文件状态 + * @param {Fs.PathLike} path 路径 + * @returns {Promise} + */ + stat: promisify(Fs.stat), + + /** + * 创建文件夹 + * @param {Fs.PathLike} path 路径 + * @param {Fs.MakeDirectoryOptions?} options 选项 + * @returns {Promise} + */ + mkdir: promisify(Fs.mkdir), + + /** + * 读取文件夹 + * @param {Fs.PathLike} path 路径 + * @param {any} options 选项 + * @returns {Promise} + */ + readdir: promisify(Fs.readdir), + + /** + * 移除文件夹 + * @param {Fs.PathLike} path 路径 + * @param {Fs.RmDirOptions?} options 选项 + * @returns {Promise} + */ + rmdir: promisify(Fs.rmdir), + + /** + * 读取文件 + * @param {Fs.PathLike} path 路径 + * @param {any} options 选项 + * @returns {Promise} + */ + readFile: promisify(Fs.readFile), + + /** + * 创建文件 + * @param {Fs.PathLike} path 路径 + * @param {string | NodeJS.ArrayBufferView} data 数据 + * @param {Fs.WriteFileOptions?} options 选项 + * @returns {Promise} + */ + writeFile: promisify(Fs.writeFile), + + /** + * 移除文件 + * @param {Fs.PathLike} path 路径 + * @returns {Promise} + */ + unlink: promisify(Fs.unlink), + + /** + * 测试路径是否存在 (同步) + * @param {Fs.PathLike} path 路径 + */ + existsSync: Fs.existsSync, + + /** + * 复制文件/文件夹 + * @param {Fs.PathLike} srcPath 源路径 + * @param {Fs.PathLike} destPath 目标路径 + * @returns {Promise} + */ + async copy(srcPath, destPath) { + if (!FileUtil.existsSync(srcPath)) { + return false; + } + const stats = await FileUtil.stat(srcPath); + if (stats.isDirectory()) { + if (!FileUtil.existsSync(destPath)) { + await FileUtil.createDir(destPath); + } + const names = await FileUtil.readdir(srcPath); + for (const name of names) { + await FileUtil.copy(Path.join(srcPath, name), Path.join(destPath, name)); + } + } else { + await FileUtil.writeFile(destPath, await FileUtil.readFile(srcPath)); + } + return true; + }, + + /** + * 创建文件夹 (递归) + * @param {Fs.PathLike} path 路径 + * @returns {Promise} + */ + async createDir(path) { + if (FileUtil.existsSync(path)) { + return true; + } else { + const dir = Path.dirname(path); + if (await FileUtil.createDir(dir)) { + await FileUtil.mkdir(path); + return true; + } + } + return false; + }, + + /** + * 移除文件/文件夹 (递归) + * @param {Fs.PathLike} path 路径 + */ + async remove(path) { + if (!FileUtil.existsSync(path)) { + return; + } + const stats = await FileUtil.stat(path); + if (stats.isDirectory()) { + const names = await FileUtil.readdir(path); + for (const name of names) { + await FileUtil.remove(Path.join(path, name)); + } + await FileUtil.rmdir(path); + } else { + await FileUtil.unlink(path); + } + }, + + /** + * 遍历文件/文件夹并执行函数 + * @param {Fs.PathLike} path 路径 + * @param {(filePath: Fs.PathLike, stat: Fs.Stats) => void | Promise} handler 处理函数 + */ + async map(path, handler) { + if (!FileUtil.existsSync(path)) { + return; + } + const stats = await FileUtil.stat(path); + if (stats.isDirectory()) { + const names = await FileUtil.readdir(path); + for (const name of names) { + await FileUtil.map(Path.join(path, name), handler); + } + } else { + await handler(path, stats); + } + }, + +}; + +module.exports = FileUtil; diff --git a/src/eazax/i18n.js b/src/eazax/i18n.js new file mode 100644 index 0000000..5790733 --- /dev/null +++ b/src/eazax/i18n.js @@ -0,0 +1,36 @@ +const zh = require('../../i18n/zh'); +const en = require('../../i18n/en'); + +/** + * 多语言 + * @author 陈皮皮 (ifaswind) + * @version 20210929 + */ +const I18n = { + + /** + * 中文 + */ + zh, + + /** + * 英文 + */ + en, + + /** + * 获取多语言文本 + * @param {string} lang 语言 + * @param {string} key 关键字 + * @returns {string} + */ + get(lang, key) { + if (I18n[lang] && I18n[lang][key]) { + return I18n[lang][key]; + } + return key; + }, + +}; + +module.exports = I18n; diff --git a/src/eazax/main-event.js b/src/eazax/main-event.js new file mode 100644 index 0000000..d74904a --- /dev/null +++ b/src/eazax/main-event.js @@ -0,0 +1,81 @@ +const { ipcMain } = require('electron'); +const PackageUtil = require('./package-util'); + +/** 包名 */ +const PACKAGE_NAME = PackageUtil.name; + +/** + * 主进程 IPC 事件 + * @author 陈皮皮 (ifaswind) + * @version 20210818 + */ +const MainEvent = { + + /** + * 监听事件(一次性) + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + once(channel, callback) { + return ipcMain.once(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 监听事件 + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + on(channel, callback) { + return ipcMain.on(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 取消事件监听 + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + removeListener(channel, callback) { + return ipcMain.removeListener(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 取消事件的所有监听 + * @param {string} channel 频道 + */ + removeAllListeners(channel) { + return ipcMain.removeAllListeners(`${PACKAGE_NAME}:${channel}`); + }, + + /** + * 发送事件到指定渲染进程 + * @param {Electron.WebContents} webContents 渲染进程事件对象 + * @param {string} channel 频道 + * @param {any[]?} args 参数 + */ + send(webContents, channel) { + // return webContents.send(`${PACKAGE_NAME}:${channel}`, ...args); + const args = [`${PACKAGE_NAME}:${channel}`]; + for (let i = 2, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return webContents.send.apply(webContents, args); + }, + + /** + * 回复事件给渲染进程 + * @param {Electron.IpcMainEvent} ipcMainEvent 事件对象 + * @param {string} channel 频道 + * @param {any[]?} args 参数 + */ + reply(ipcMainEvent, channel) { + // return ipcMainEvent.reply(`${PACKAGE_NAME}:${channel}`, ...args); + const args = [`${PACKAGE_NAME}:${channel}`]; + for (let i = 2, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return ipcMainEvent.reply.apply(ipcMainEvent, args); + }, + +}; + +module.exports = MainEvent; diff --git a/src/eazax/package-util.js b/src/eazax/package-util.js new file mode 100644 index 0000000..18e9839 --- /dev/null +++ b/src/eazax/package-util.js @@ -0,0 +1,47 @@ +const { shell } = require('electron'); + +/** 包信息 */ +const PACKAGE_JSON = require('../../package.json'); + +/** + * 包工具 + * @author 陈皮皮 (ifaswind) + * @version 20210908 + */ +const PackageUtil = { + + /** + * 包名 + * @type {string} + */ + get name() { + return PACKAGE_JSON.name; + }, + + /** + * 版本 + * @type {string} + */ + get version() { + return PACKAGE_JSON.version; + }, + + /** + * 仓库地址 + * @type {string} + */ + get repository() { + return PACKAGE_JSON.repository; + }, + + /** + * 打开仓库页面 + */ + openRepository() { + const url = PackageUtil.repository; + shell.openExternal(url); + }, + +}; + +module.exports = PackageUtil; diff --git a/src/eazax/renderer-event.js b/src/eazax/renderer-event.js new file mode 100644 index 0000000..82d84f0 --- /dev/null +++ b/src/eazax/renderer-event.js @@ -0,0 +1,80 @@ +const { ipcRenderer } = require('electron'); +const PackageUtil = require('./package-util'); + +/** 包名 */ +const PACKAGE_NAME = PackageUtil.name; + +/** + * 渲染进程 IPC 事件 + * @author 陈皮皮 (ifaswind) + * @version 20210818 + */ +const RendererEvent = { + + /** + * 监听事件(一次性) + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + once(channel, callback) { + return ipcRenderer.once(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 监听事件 + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + on(channel, callback) { + return ipcRenderer.on(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 取消事件监听 + * @param {string} channel 频道 + * @param {Function} callback 回调 + */ + removeListener(channel, callback) { + return ipcRenderer.removeListener(`${PACKAGE_NAME}:${channel}`, callback); + }, + + /** + * 取消事件的所有监听 + * @param {string} channel 频道 + */ + removeAllListeners(channel) { + return ipcRenderer.removeAllListeners(`${PACKAGE_NAME}:${channel}`); + }, + + /** + * 发送事件到主进程 + * @param {string} channel 频道 + * @param {...any} args 参数 + */ + send(channel) { + // return ipcRenderer.send(`${PACKAGE_NAME}:${channel}`, ...args); + const args = [`${PACKAGE_NAME}:${channel}`]; + for (let i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return ipcRenderer.send.apply(ipcRenderer, args); + }, + + /** + * 发送事件到主进程(同步) + * @param {string} channel 频道 + * @param {...any} args 参数 + * @returns {Promise} + */ + sendSync(channel) { + // return ipcRenderer.sendSync(`${PACKAGE_NAME}:${channel}`, ...args); + const args = [`${PACKAGE_NAME}:${channel}`]; + for (let i = 1, l = arguments.length; i < l; i++) { + args.push(arguments[i]); + } + return ipcRenderer.sendSync.apply(ipcRenderer, args); + }, + +}; + +module.exports = RendererEvent; diff --git a/src/eazax/updater.js b/src/eazax/updater.js new file mode 100644 index 0000000..67ee24a --- /dev/null +++ b/src/eazax/updater.js @@ -0,0 +1,92 @@ +const fetch = require('../../lib/node-fetch'); +const PackageUtil = require('./package-util'); +const { compareVersion } = require('./version-util'); + +/** 本地版本 */ +const LOCAL_VERSION = PackageUtil.version; + +/** 远程仓库地址 */ +const REMOTE_URL = PackageUtil.repository; + +/** + * 更新器 + * @author 陈皮皮 (ifaswind) + * @version 20210804 + */ +const Updater = { + + /** + * 远程仓库地址 + * @type {string} + */ + get remote() { + return REMOTE_URL; + }, + + /** + * 分支 + * @type {string} + */ + branch: 'master', + + /** + * 获取远端的 package.json + * @returns {Promise} + */ + async getRemotePackageJson() { + const packageJsonUrl = `${Updater.remote}/raw/${Updater.branch}/package.json`; + // 发起网络请求 + const response = await fetch(packageJsonUrl, { + method: 'GET', + cache: 'no-cache', + mode: 'no-cors', + }); + // 请求结果 + if (response.status !== 200) { + return null; + } + // 读取 json + const json = response.json(); + return json; + }, + + /** + * 获取远端版本号 + * @returns {Promise} + */ + async getRemoteVersion() { + const package = await Updater.getRemotePackageJson(); + if (package && package.version) { + return package.version; + } + return null; + }, + + /** + * 获取本地版本号 + * @returns {string} + */ + getLocalVersion() { + return LOCAL_VERSION; + }, + + /** + * 检查远端是否有新版本 + * @returns {Promise} + */ + async check() { + // 远端版本号 + const remoteVersion = await Updater.getRemoteVersion(); + if (!remoteVersion) { + return false; + } + // 本地版本号 + const localVersion = Updater.getLocalVersion(); + // 对比版本号 + const result = compareVersion(localVersion, remoteVersion); + return (result < 0); + }, + +}; + +module.exports = Updater; diff --git a/src/eazax/version-util.js b/src/eazax/version-util.js new file mode 100644 index 0000000..30bd303 --- /dev/null +++ b/src/eazax/version-util.js @@ -0,0 +1,61 @@ +/** + * 版本工具 + * @author 陈皮皮 (ifaswind) + * @version 20210814 + */ +const VersionUtil = { + + /** + * 拆分版本号 + * @param {string | number} version 版本号文本 + * @returns {number[]} + * @example + * splitVersionString('1.2.0'); // [1, 2, 0] + */ + splitVersionString(version) { + if (typeof version === 'number') { + return [version]; + } + if (typeof version === 'string') { + return ( + version.replace(/-/g, '.') + .split('.') + .map(v => (parseInt(v) || 0)) + ); + } + return [0]; + }, + + /** + * 对比版本号 + * @param {string | number} a 版本 a + * @param {string | number} b 版本 b + * @returns {-1 | 0 | 1} + * @example + * compareVersion('1.0.0', '1.0.1'); // -1 + * compareVersion('1.1.0', '1.1.0'); // 0 + * compareVersion('1.2.1', '1.2.0'); // 1 + * compareVersion('1.2.0.1', '1.2.0'); // 1 + */ + compareVersion(a, b) { + const acs = VersionUtil.splitVersionString(a), + bcs = VersionUtil.splitVersionString(b); + const count = Math.max(acs.length, bcs.length); + for (let i = 0; i < count; i++) { + const ac = acs[i], + bc = bcs[i]; + // 前者缺少分量或前者小于后者 + if (ac == undefined || ac < bc) { + return -1; + } + // 后者缺少分量或前者大于后者 + if (bc == undefined || ac > bc) { + return 1; + } + } + return 0; + }, + +}; + +module.exports = VersionUtil; diff --git a/src/eazax/window-util.js b/src/eazax/window-util.js new file mode 100644 index 0000000..b6abd03 --- /dev/null +++ b/src/eazax/window-util.js @@ -0,0 +1,80 @@ +const { BrowserWindow } = require('electron'); + +/** + * 窗口工具(主进程) + * @author 陈皮皮 (ifaswind) + * @version 20210825 + */ +const WindowUtil = { + + /** + * 最先打开的窗口 + * @returns {BrowserWindow} + */ + getFirstWindow() { + const wins = BrowserWindow.getAllWindows(); + return wins[wins.length - 1]; + }, + + /** + * 获取当前聚焦的窗口 + * @returns {BrowserWindow} + */ + getFocusedWindow() { + return BrowserWindow.getFocusedWindow(); + }, + + /** + * 计算窗口位置(相对于最先打开的窗口) + * @param {[number, number]} size 窗口尺寸 + * @param {'top' | 'center'} anchor 锚点 + * @returns {[number, number]} + */ + calcWindowPosition(size, anchor) { + const win = WindowUtil.getFirstWindow(); + return WindowUtil.calcWindowPositionByTarget(size, anchor, win); + }, + + /** + * 计算窗口位置(相对于当前聚焦的窗口) + * @param {[number, number]} size 窗口尺寸 + * @param {'top' | 'center'} anchor 锚点 + * @returns {[number, number]} + */ + calcWindowPositionByFocused(size, anchor) { + const win = WindowUtil.getFocusedWindow(); + return WindowUtil.calcWindowPositionByTarget(size, anchor, win); + }, + + /** + * 计算窗口位置(相对于当前聚焦的窗口) + * @param {[number, number]} size 窗口尺寸 + * @param {'top' | 'center'} anchor 锚点 + * @param {BrowserWindow} win 目标窗口 + * @returns {[number, number]} + */ + calcWindowPositionByTarget(size, anchor, win) { + // 根据目标窗口的位置和尺寸来计算 + const winSize = win.getSize(), + winPos = win.getPosition(); + // 注意:原点 (0, 0) 在屏幕左上角 + // 另外,窗口的位置值必须是整数,否则修改无效(像素的最小粒度为 1) + const x = Math.floor(winPos[0] + (winSize[0] / 2) - (size[0] / 2)); + let y; + switch (anchor) { + case 'top': { + y = Math.floor(winPos[1]); + break; + } + default: + case 'center': { + y = Math.floor(winPos[1] + (winSize[1] / 2) - (size[1] / 2)); + break; + } + } + return [x, y]; + }, + +}; + +module.exports = WindowUtil; diff --git a/src/main/editor-api.js b/src/main/editor-api.js new file mode 100644 index 0000000..7fd3c96 --- /dev/null +++ b/src/main/editor-api.js @@ -0,0 +1,98 @@ +/** + * 编辑器 API(用于抹平不同版本编辑器之间的差异) + * @author 陈皮皮 (ifaswind) + * @version 20210830 + */ +const EditorAPI = { + + /** + * 当前语言 + * @returns {string} + */ + getLanguage() { + return Editor.lang || Editor.I18n.getLanguage(); + }, + + /** + * 绝对路径转为UUID + * @param {string} fspath + */ + fspathToUuid(fspath) { + return Editor.assetdb.fspathToUuid(fspath); + }, + + /** + * 绝对路径转为编辑器资源路径 + * @param {string} fspath + */ + fspathToUrl(fspath) { + return Editor.assetdb.fspathToUrl(fspath); + }, + + /** + * 编辑器资源路径转为绝对路径 + * @param {string} url + */ + urlToFspath(url) { + return Editor.assetdb.urlToFspath(url); + }, + + /** + * 通过 uuid 获取资源信息 + * @param {string} uuid + */ + assetInfoByUuid(uuid) { + return Editor.assetdb.assetInfoByUuid(uuid); + }, + + /** + * 通过 uuid 获取子资源信息 + * @param {string} uuid + */ + subAssetInfosByUuid(uuid) { + return Editor.assetdb.subAssetInfosByUuid(uuid); + }, + + /** + * 获取当前选中的资源 uuid + * @returns {string[]} + */ + getCurrentSelectedAssets() { + return Editor.Selection.curSelection('asset'); + }, + + /** + * 获取当前选中的节点 uuid + * @returns {string[]} + */ + getCurrentSelectedNodes() { + return Editor.Selection.curSelection('node'); + }, + + /** + * 是否为 uuid + * @param {string} uuid + */ + isUuid(uuid) { + return Editor.Utils.UuidUtils.isUuid(uuid); + }, + + /** + * 压缩 uuid + * @param {string} uuid + */ + compressUuid(uuid) { + return Editor.Utils.UuidUtils.compressUuid(uuid); + }, + + /** + * 反压缩 uuid + * @param {string} uuid + */ + decompressUuid(uuid) { + return Editor.Utils.UuidUtils.decompressUuid(uuid); + }, + +}; + +module.exports = EditorAPI; diff --git a/src/main/finder.js b/src/main/finder.js new file mode 100644 index 0000000..69fc990 --- /dev/null +++ b/src/main/finder.js @@ -0,0 +1,248 @@ +const { extname, basename } = require("path"); +const EditorAPI = require("./editor-api"); +const { print, translate } = require("../eazax/editor-main-util"); +const FileUtil = require("../eazax/file-util"); +const { containsValue } = require("./object-util"); +const Parser = require("./parser"); + +/** 扩展名对应文件类型 */ +const ASSET_TYPE_MAP = { + // 场景 + '.fire': 'scene', + '.scene': 'scene', + // 预制体 + '.prefab': 'prefab', + // 动画 + '.anim': 'animation', + // 材质 + '.mtl': 'material', + // 字体 + '.fnt.meta': 'font', +}; + +/** + * 查找器 + */ +const Finder = { + + /** + * 使用 uuid 进行查找 + * @param {string} uuid + */ + async findByUuid(uuid) { + // 是否为有效 uuid + if (!EditorAPI.isUuid(uuid)) { + print('log', translate('invalid-uuid'), uuid); + return []; + } + // 获取资源信息 + const assetInfo = EditorAPI.assetInfoByUuid(uuid); + if (assetInfo) { + // 记录子资源 uuid + const subAssetUuids = []; + // 资源类型检查 + if (assetInfo.type === 'texture') { + // 纹理子资源 + const subAssetInfos = EditorAPI.subAssetInfosByUuid(uuid); + if (subAssetInfos && subAssetInfos.length > 0) { + for (let i = 0; i < subAssetInfos.length; i++) { + subAssetUuids.push(subAssetInfos[i].uuid); + } + } + } else if (assetInfo.type === 'typescript' || assetInfo.type === 'javascript') { + // 脚本资源 + uuid = EditorAPI.compressUuid(uuid); + } + // 查找资源引用 + const results = [], + selfResults = await Finder.findRefs(uuid); + for (let i = 0, l = selfResults.length; i < l; i++) { + results.push(selfResults[i]); + } + // 查找子资源的引用 + if (subAssetUuids.length > 0) { + for (let i = 0, l = subAssetUuids.length; i < l; i++) { + const subResults = await Finder.findRefs(subAssetUuids[i]); + for (let j = 0, l = subResults.length; j < l; j++) { + results.push(subResults[j]); + } + } + } + return results; + } else { + // 不存在的资源,直接查找 uuid + print('log', translate('find-asset-refs'), uuid); + return (await Finder.findRefs(uuid)); + } + }, + + /** + * 查找引用 + * @param {string} uuid + * @returns {Promise<{ type: string, url: string, refs?: object[]}[]>} + */ + async findRefs(uuid) { + const result = []; + // 文件处理函数 + const handler = async (path, stats) => { + const ext = extname(path); + if (ext === '.fire' || ext === '.scene' || ext === '.prefab') { + // 场景和预制体资源(转为节点树) + const tree = await Parser.getNodeTree(path); + if (!tree) { + return; + } + // 遍历第一层节点查找引用 + const refs = []; + for (let children = tree.children, i = 0, l = children.length; i < l; i++) { + Finder.findRefsInNode(tree, children[i], uuid, refs); + } + // 保存当前文件引用结果 + if (refs.length > 0) { + result.push({ + type: ASSET_TYPE_MAP[ext], + url: EditorAPI.fspathToUrl(path), + refs: refs, + }); + } + } else if (ext === '.anim') { + // 动画资源 + const data = JSON.parse(await FileUtil.readFile(path)), + curveData = data['curveData'], + contains = containsValue(curveData, uuid); + if (contains) { + result.push({ + type: ASSET_TYPE_MAP[ext], + url: EditorAPI.fspathToUrl(path), + }); + } + } else if (ext === '.mtl' || path.endsWith('.fnt.meta')) { + // 材质和字体资源 + const data = JSON.parse(await FileUtil.readFile(path)); + // 需排除自己 + if ((data['uuid'] === uuid)) { + return; + } + // 是否引用 + const contains = containsValue(data, uuid); + if (contains) { + const _ext = (ext === '.mtl') ? '.mtl' : '.fnt.meta'; + result.push({ + type: ASSET_TYPE_MAP[_ext], + url: EditorAPI.fspathToUrl(path), + }); + } + } + }; + // 遍历资源目录下的文件 + const assetsPath = EditorAPI.urlToFspath('db://assets'); + await FileUtil.map(assetsPath, handler); + return result; + }, + + /** + * 查找节点中的引用 + * @param {object} tree 节点树 + * @param {object} node 目标节点 + * @param {string} uuid 查找的 uuid + * @param {object[]} result 结果 + */ + findRefsInNode(tree, node, uuid, result) { + // 检查节点上的组件是否有引用 + const components = node.components; + if (components && components.length > 0) { + for (let i = 0, l = components.length; i < l; i++) { + const properties = Finder.getContainsUuidProperties(components[i], uuid); + if (properties.length === 0) { + continue; + } + // 资源类型 + let type = components[i]['__type__']; + // 是否为脚本资源 + if (EditorAPI.isUuid(type)) { + const scriptUuid = EditorAPI.decompressUuid(type), + assetInfo = EditorAPI.assetInfoByUuid(scriptUuid); + type = basename(assetInfo.url); + } + // 遍历相关属性名 + for (let i = 0; i < properties.length; i++) { + let property = properties[i]; + if (property === '__type__') { + property = null; + } else { + // 处理属性名称(Label 组件需要特殊处理) + if (type === 'cc.Label' && property === '_N$file') { + property = 'font'; + } + // 去除属性名的前缀 + if (property.startsWith('_N$')) { + property = property.replace('_N$', ''); + } else if (property[0] === '_') { + property = property.substring(1); + } + } + // 保存结果 + result.push({ + node: node.path, + component: type, + property: property, + }); + } + } + } + // 检查预制体是否有引用 + const prefab = node.prefab; + if (prefab) { + // 排除预制体自己 + if (uuid !== tree.uuid) { + const contains = containsValue(prefab, uuid); + if (contains) { + result.push({ + node: node.path, + }); + } + } + } + // 遍历子节点 + const children = node.children; + if (children && children.length > 0) { + for (let i = 0, l = children.length; i < l; i++) { + Finder.findRefsInNode(tree, children[i], uuid, result); + } + } + }, + + /** + * 获取对象包含指定 uuid 的属性 + * @param {object} object 对象 + * @param {string} uuid 值 + * @returns {string[]} + */ + getContainsUuidProperties(object, uuid) { + const properties = []; + const search = (target, path) => { + if (Object.prototype.toString.call(target) === '[object Object]') { + for (const key in target) { + const curPath = (path != null) ? `${path}.${key}` : key; + if (target[key] === uuid) { + properties.push(path || key); + } + search(target[key], curPath); + } + } else if (Array.isArray(target)) { + for (let i = 0, l = target.length; i < l; i++) { + const curPath = (path != null) ? `${path}[${i}]` : `[${i}]`; + if (target[i] === uuid) { + properties.push(path || `[${i}]`); + } + search(target[i], curPath); + } + } + } + search(object, null); + return properties; + }, + +}; + +module.exports = Finder; diff --git a/src/main/index.js b/src/main/index.js new file mode 100644 index 0000000..48319c9 --- /dev/null +++ b/src/main/index.js @@ -0,0 +1,155 @@ +const PanelManager = require('./panel-manager'); +const ConfigManager = require('../common/config-manager'); +const EditorMainKit = require('../eazax/editor-main-kit'); +const { checkUpdate, print, translate } = require('../eazax/editor-main-util'); +const { openRepository } = require('../eazax/package-util'); +const EditorAPI = require('./editor-api'); +const Parser = require('./parser'); +const Finder = require('./finder'); +const Printer = require('./printer'); + +/** + * 生命周期:加载 + */ +function load() { + // 监听事件 + EditorMainKit.register(); +} + +/** + * 生命周期:卸载 + */ +function unload() { + // 取消事件监听 + EditorMainKit.unregister(); +} + +/** + * 查找当前选中资源 + */ +async function findCurrentSelection() { + // 过滤选中的资源 uuid + const uuids = EditorAPI.getCurrentSelectedAssets(); + for (let i = 0; i < uuids.length; i++) { + const assetInfo = EditorAPI.assetInfoByUuid(uuids[i]); + if (assetInfo.type === 'folder') { + uuids.splice(i--); + } + } + // 未选择资源 + if (uuids.length === 0) { + print('log', translate('please-select-assets')); + return; + } + // 遍历查找 + for (let i = 0; i < uuids.length; i++) { + const uuid = uuids[i], + assetInfo = EditorAPI.assetInfoByUuid(uuid), + shortUrl = assetInfo.url.replace('db://', ''); + // 查找引用 + print('log', '🔍', `${translate('find-asset-refs')} ${shortUrl}`); + const refs = await Finder.findByUuid(uuid); + if (refs.length === 0) { + print('log', '📂', `${translate('no-refs')} ${shortUrl}`); + continue; + } + // 打印结果 + Printer.printResult({ + type: assetInfo.type, + uuid: uuid, + url: assetInfo.url, + path: assetInfo.path, + refs: refs, + }); + } +} + +function getSelection() { + +} + +/** + * 资源变化回调 + * @param {{ type: string, uuid: string }} info + */ +function onAssetChanged(info) { + const { type, uuid } = info; + // 场景和预制体 + if (type === 'scene' || type === 'prefab') { + const { url, path } = EditorAPI.assetInfoByUuid(uuid); + // 排除内置资源 + if (url.indexOf('db://internal') !== -1) { + return; + } + // 更新节点树 + Parser.updateCache(path); + } +} + +module.exports = { + + /** + * 扩展消息 + */ + messages: { + + /** + * 查找当前选中资源 + * @param {*} event + */ + 'find-current-selection'(event) { + findCurrentSelection(); + }, + + /** + * 打开设置面板 + * @param {*} event + */ + 'open-settings-panel'(event) { + PanelManager.openSettingsPanel(); + }, + + /** + * 检查更新 + * @param {*} event + */ + 'menu-check-update'(event) { + checkUpdate(true); + }, + + /** + * 版本 + * @param {*} event + */ + 'menu-version'(event) { + openRepository(); + }, + + /** + * 场景面板加载完成后 + * @param {*} event + */ + 'scene:ready'(event) { + // 自动检查更新 + const config = ConfigManager.get(); + if (config.autoCheckUpdate) { + checkUpdate(false); + } + }, + + /** + * 资源变化 + * @param {*} event + * @param {{ type: string, uuid: string }} info + */ + 'asset-db:asset-changed'(event, info) { + onAssetChanged(info); + }, + + }, + + load, + + unload, + +}; diff --git a/src/main/object-util.js b/src/main/object-util.js new file mode 100644 index 0000000..ee2ef48 --- /dev/null +++ b/src/main/object-util.js @@ -0,0 +1,70 @@ +/** + * 对象工具 + * @author 陈皮皮 (ifaswind) + * @version 20210929 + */ +const ObjectUtil = { + + /** + * 判断指定值是否是一个对象 + * @param {any} arg 参数 + */ + isObject(arg) { + return Object.prototype.toString.call(arg) === '[object Object]'; + }, + + /** + * 对象中是否包含指定的属性 + * @param {object} object 对象 + * @param {string} name 属性名 + */ + containsProperty(object, name) { + let result = false; + const search = (_object) => { + if (ObjectUtil.isObject(_object)) { + for (const key in _object) { + if (key == name) { + result = true; + return; + } + search(_object[key]); + } + } else if (Array.isArray(_object)) { + for (let i = 0, l = _object.length; i < l; i++) { + search(_object[i]); + } + } + } + search(object); + return result; + }, + + /** + * 对象中是否包含指定的值 + * @param {object} object 对象 + * @param {any} value 值 + */ + containsValue(object, value) { + let result = false; + const search = (_object) => { + if (ObjectUtil.isObject(_object)) { + for (const key in _object) { + if (_object[key] === value) { + result = true; + return; + } + search(_object[key]); + } + } else if (Array.isArray(_object)) { + for (let i = 0, l = _object.length; i < l; i++) { + search(_object[i]); + } + } + } + search(object); + return result; + }, + +}; + +module.exports = ObjectUtil; diff --git a/src/main/panel-manager.js b/src/main/panel-manager.js new file mode 100644 index 0000000..6374b02 --- /dev/null +++ b/src/main/panel-manager.js @@ -0,0 +1,86 @@ +const { BrowserWindow } = require('electron'); +const { join } = require('path'); +const { language, translate } = require('../eazax/editor-main-util'); +const { calcWindowPosition } = require('../eazax/window-util'); + +/** 扩展名称 */ +const EXTENSION_NAME = translate('name'); + +/** + * 面板管理器 (主进程) + */ +const PanelManager = { + + /** + * 面板实例 + * @type {BrowserWindow} + */ + settings: null, + + /** + * 打开设置面板 + */ + openSettingsPanel() { + // 已打开则直接展示 + if (PanelManager.settings) { + PanelManager.settings.show(); + return; + } + // 窗口尺寸和位置 + const winSize = [500, 346], + winPos = calcWindowPosition(winSize, 'center'); + // 创建窗口 + const win = PanelManager.settings = new BrowserWindow({ + width: winSize[0], + height: winSize[1], + minWidth: winSize[0], + minHeight: winSize[1], + x: winPos[0], + y: winPos[1] - 100, + useContentSize: true, + frame: true, + title: `${EXTENSION_NAME} | Cocos Creator`, + autoHideMenuBar: true, + resizable: true, + minimizable: false, + maximizable: false, + fullscreenable: false, + skipTaskbar: false, + alwaysOnTop: true, + hasShadow: true, + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + }, + }); + // 就绪后(展示,避免闪烁) + win.on('ready-to-show', () => win.show()); + // 关闭后 + win.on('closed', () => (PanelManager.settings = null)); + // 监听按键 + win.webContents.on('before-input-event', (event, input) => { + if (input.key === 'Escape') PanelManager.closeSettingsPanel(); + }); + // 调试用的 devtools + // win.webContents.openDevTools({ mode: 'detach' }); + // 加载页面 + const path = join(__dirname, '../renderer/settings/index.html'); + win.loadURL(`file://${path}?lang=${language}`); + }, + + /** + * 关闭面板 + */ + closeSettingsPanel() { + if (!PanelManager.settings) { + return; + } + PanelManager.settings.hide(); + PanelManager.settings.close(); + PanelManager.settings = null; + }, + +}; + +module.exports = PanelManager; diff --git a/src/main/parser.js b/src/main/parser.js new file mode 100644 index 0000000..fd582b2 --- /dev/null +++ b/src/main/parser.js @@ -0,0 +1,161 @@ +const { print } = require("../eazax/editor-main-util"); +const FileUtil = require("../eazax/file-util"); +const { containsProperty } = require("./object-util"); + +/** + * 解析器 + */ +const Parser = { + + /** + * 节点树缓存 + * @type {{ [key: string]: object }} + */ + caches: Object.create(null), + + /** + * 获取节点树 + * @param {string} path 路径 + * @returns {Promise} + */ + async getNodeTree(path) { + if (!Parser.caches[path]) { + const file = await FileUtil.readFile(path); + let data = null; + try { + data = JSON.parse(file); + } catch (error) { + print('warn', '文件解析失败', path); + print('warn', error); + } + if (!data) { + return null; + } + Parser.caches[path] = Parser.convert(data); + } + return Parser.caches[path]; + }, + + /** + * 更新缓存 + * @param {string} path 路径 + */ + async updateCache(path) { + Parser.caches[path] = null; + await Parser.getNodeTree(path); + }, + + /** + * 将资源解析为节点树 + * @param {object} source 源数据 + * @returns {object} + */ + convert(source) { + const tree = Object.create(null), + type = source[0]['__type__']; + if (type === 'cc.SceneAsset') { + // 场景资源 + const sceneId = source[0]['scene']['__id__'], + children = source[sceneId]['_children']; + tree.type = 'cc.Scene'; // 类型 + tree.id = sceneId; // ID + // 场景下可以有多个一级节点 + tree.children = []; + for (let i = 0, l = children.length; i < l; i++) { + const nodeId = children[i]['__id__']; + Parser.convertNode(source, nodeId, tree); + } + } else if (type === 'cc.Prefab') { + // 预制体资源 + const uuid = source[source.length - 1]['asset']['__uuid__']; + tree.type = 'cc.Prefab'; // 类型 + tree.uuid = uuid; // uuid + // 预制体本身就是一个节点 + tree.children = []; + const nodeId = source[0]['data']['__id__']; + Parser.convertNode(source, nodeId, tree); + } + return tree; + }, + + /** + * 解析节点 + * @param {object} source 源数据 + * @param {number} nodeId 节点 ID + * @param {object} parent 父节点 + */ + convertNode(source, nodeId, parent) { + const srcNode = source[nodeId], + node = Object.create(null); + // 基本信息 + node.name = srcNode['_name']; + node.id = nodeId; + node.type = srcNode['__type__']; + + // 路径 + const parentPath = parent.path || null; + node.path = parentPath ? `${parentPath}/${node.name}` : node.name; + + // 预制体引用 + const srcPrefab = srcNode['_prefab']; + if (srcPrefab) { + const id = srcPrefab['__id__']; + node.prefab = Parser.extractValidInfo(source[id]); + } + + // 组件 + node.components = []; + const srcComponents = srcNode['_components']; + if (srcComponents && srcComponents.length > 0) { + for (let i = 0, l = srcComponents.length; i < l; i++) { + const compId = srcComponents[i]['__id__'], + component = Parser.extractValidInfo(source[compId]); + node.components.push(component); + } + } + + // 子节点 + node.children = []; + const srcChildren = srcNode['_children']; + if (srcChildren && srcChildren.length > 0) { + for (let i = 0, l = srcChildren.length; i < l; i++) { + const nodeId = srcChildren[i]['__id__']; + Parser.convertNode(source, nodeId, node); + } + } + + // 保存到父节点 + parent.children.push(node); + }, + + /** + * 提取有效信息(含有 uuid) + * @param {object} source 源数据 + * @returns {{ __type__: string, _name: string, fileId?: string }} + */ + extractValidInfo(source) { + const result = Object.create(null); + + // 记录有用的属性 + const keys = ['__type__', '_name', 'fileId']; + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + if (source[key] !== undefined) { + result[key] = source[key]; + } + } + + // 记录包含 uuid 的属性 + for (const key in source) { + const contains = containsProperty(source[key], '__uuid__'); + if (contains) { + result[key] = source[key]; + } + } + + return result; + }, + +}; + +module.exports = Parser; diff --git a/src/main/printer.js b/src/main/printer.js new file mode 100644 index 0000000..ef993ae --- /dev/null +++ b/src/main/printer.js @@ -0,0 +1,108 @@ +const { translate, print, pureWithoutTitle } = require('../eazax/editor-main-util'); +const ConfigManager = require('../common/config-manager'); + +/** 图标表 */ +const ICON_MAP = { + 'scene': '🔥', + 'prefab': '💠', + 'node': '🎲', + 'component': '🧩', + 'property': '📄', + 'asset': '📦', + 'asset-info': '📋', + 'node-refs': '📙', + 'asset-refs': '📗', +}; + +/** + * 打印机 + */ +const Printer = { + + /** + * 打印结果至控制台 + * @param {object} result + */ + printResult(result) { + if (!result) { + return; + } + const { printDetails, printFolding } = ConfigManager.get(); + // 标志位 + const nodeRefs = [], assetRefs = []; + let nodeRefsCount = 0, assetRefsCount = 0; + // 遍历引用信息 + for (let refs = result.refs, i = 0, l = refs.length; i < l; i++) { + const ref = refs[i], + type = ref.type, + url = ref.url.replace('db://', '').replace('.meta', ''); + if (type === 'scene' || type === 'prefab') { + // 场景或预制体 + nodeRefs.push(`  ${ICON_MAP[type]} [${translate(type)}] ${url}`); + // 节点引用 + for (let details = ref.refs, j = 0, l = details.length; j < l; j++) { + nodeRefsCount++; + // 详情 + if (printDetails) { + const detail = details[j]; + let item = `    ${ICON_MAP['node']} [${translate('node')}] ${detail.node}`; + if (detail.component) { + item += `  →  ${ICON_MAP['component']} [${translate('component')}] ${detail.component}`; + } + if (detail.property) { + item += `  →  ${ICON_MAP['property']} [${translate('property')}] ${detail.property}`; + } + nodeRefs.push(item); + } + } + } else { + // 资源引用 + assetRefsCount++; + assetRefs.push(`  ${ICON_MAP['asset']} [${translate(type)}] ${url}`); + } + } + // 组装文本 + const texts = []; + // 分割线 + texts.push(`${'- - '.repeat(36)}`); + // 基础信息 + texts.push(`${ICON_MAP['asset-info']} ${translate('asset-info')}`); + texts.push(`  - ${translate('asset-type')}${result.type}`); + texts.push(`  - ${translate('asset-uuid')}${result.uuid}`); + texts.push(`  - ${translate('asset-url')}${result.url}`); + texts.push(`  - ${translate('asset-path')}${result.path}`); + // 分割线 + texts.push(`${'- - '.repeat(36)}`); + // 节点引用 + if (nodeRefs.length > 0) { + texts.push(`${ICON_MAP['node-refs']} ${translate('node-refs')} x ${nodeRefsCount}`); + for (let i = 0, l = nodeRefs.length; i < l; i++) { + texts.push(nodeRefs[i]); + } + } + // 资源引用 + if (assetRefs.length > 0) { + texts.push(`${ICON_MAP['asset-refs']} ${translate('asset-refs')} x ${assetRefsCount}`); + for (let i = 0, l = assetRefs.length; i < l; i++) { + texts.push(assetRefs[i]); + } + } + // 结尾分割线 + texts.push(`${'- - '.repeat(36)}`); + // 打印到控制台 + if (printFolding) { + // 单行打印 + texts.unshift(`🗂 ${translate('result')} >>>`); + print('log', texts.join('\n')); + } else { + // 逐行打印 + print('log', translate('result')); + for (let i = 0, l = texts.length; i < l; i++) { + pureWithoutTitle(`  ${texts[i]}`); + } + } + }, + +}; + +module.exports = Printer; diff --git a/src/renderer/settings/index.css b/src/renderer/settings/index.css new file mode 100644 index 0000000..2a0e53a --- /dev/null +++ b/src/renderer/settings/index.css @@ -0,0 +1,61 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0 12px; + background-color: #454545; + color: #bdbdbd; + user-select: none; +} + +#app { + width: 100%; + height: 100%; +} + +/* 标题 */ +.title { + font-size: 20px; + font-weight: 800; + padding: 10px 0; +} + +/* 属性容器 */ +.properties { + overflow: visible; +} + +/* 应用按钮 */ +.apply-btn { + min-width: 20px; + height: 33px; + background-image: linear-gradient(#4281b6, #4281b6); + border: 1px solid #171717; + border-radius: 3px; + color: #fff; + font-size: 16px; + font-weight: 800; + text-align: center; + outline: none; + overflow: hidden; + cursor: pointer; +} + +.apply-btn:hover { + background-image: none; + background-color: #4c87b6; +} + +.apply-btn:active { + background-image: none; + background-color: #2e6da2; + border-color: #fd942b; + color: #cdcdcd; + box-shadow: 1px 1px 10px #262626 inset; +} + +[v-cloak] { + display: none; +} diff --git a/src/renderer/settings/index.html b/src/renderer/settings/index.html new file mode 100644 index 0000000..46b0e32 --- /dev/null +++ b/src/renderer/settings/index.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + +
+ +
{{ t('settings') }}
+ +
+ +
+
+ {{ t('select-key') }} + {{ t('select-key-tooltip') }} +
+
+ +
+
+ +
+
+ {{ t('custom-key') }} + {{ t('custom-key-tooltip') }} +
+
+ +
+
+ +
+
+ {{ t('print-details') }} + {{ t('print-details-tooltip') }} +
+
+ +
+
+ +
+
+ {{ t('print-folding') }} + {{ t('print-folding-tooltip') }} +
+
+ +
+
+ +
+
+ {{ t('auto-check-update') }} + {{ t('auto-check-update-tooltip') }} +
+
+ +
+
+ +
+ {{ t('reference') }} + {{ t('accelerator') }} +
+ +
+ {{ t('repository') }} + {{ packageName }} +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/src/renderer/settings/index.js b/src/renderer/settings/index.js new file mode 100644 index 0000000..a508ebe --- /dev/null +++ b/src/renderer/settings/index.js @@ -0,0 +1,191 @@ +const { shell } = require('electron'); +const { getUrlParam } = require('../../eazax/browser-util'); +const I18n = require('../../eazax/i18n'); +const RendererEvent = require('../../eazax/renderer-event'); +const PackageUtil = require('../../eazax/package-util'); +const EditorRendererKit = require('../../eazax/editor-renderer-kit'); +const ConfigManager = require('../../common/config-manager'); + +// 导入 Vue 工具函数 +const { ref, watch, onMounted, onBeforeUnmount, createApp } = Vue; + +/** 当前语言 */ +const LANG = getUrlParam('lang'); + +// 构建 Vue 应用 +const App = { + + /** + * 设置 + * @param {*} props + * @param {*} context + */ + setup(props, context) { + + // 预设快捷键 + const presets = ref([ + { key: '', name: t('none') }, + { key: 'custom', name: t('custom-key') }, + { key: 'F1', name: 'F1' }, + { key: 'F3', name: 'F3' }, + { key: 'F4', name: 'F4' }, + { key: 'F5', name: 'F5' }, + { key: 'F6', name: 'F6' }, + { key: 'CmdOrCtrl+F', name: 'Cmd/Ctrl + F' }, + { key: 'CmdOrCtrl+B', name: 'Cmd/Ctrl + B' }, + { key: 'CmdOrCtrl+Shift+F', name: 'Cmd/Ctrl + Shift + F' }, + ]); + // 选择 + const selectKey = ref(''); + // 自定义 + const customKey = ref(''); + // 打印详情 + const printDetails = ref(true); + // 单行打印 + const printFolding = ref(true); + // 自动检查更新 + const autoCheckUpdate = ref(false); + + // 仓库地址 + const repositoryUrl = PackageUtil.repository; + // 包名 + const packageName = PackageUtil.name; + + // 监听选择快捷键 + watch(selectKey, (value) => { + if (value !== 'custom') { + customKey.value = ''; + } + }); + + // 监听自定义 + watch(customKey, (value) => { + if (value !== '' && selectKey.value !== 'custom') { + selectKey.value = 'custom'; + } + }); + + /** + * 获取配置 + */ + function getConfig() { + const config = ConfigManager.get(); + if (!config) return; + // 配置 + printDetails.value = config.printDetails; + printFolding.value = config.printFolding; + autoCheckUpdate.value = config.autoCheckUpdate; + // 快捷键 + const hotkey = config.hotkey; + if (!hotkey || hotkey === '') { + selectKey.value = ''; + customKey.value = ''; + return; + } + // 预设快捷键 + for (let i = 0, l = presets.value.length; i < l; i++) { + if (presets.value[i].key === hotkey) { + selectKey.value = hotkey; + customKey.value = ''; + return; + } + } + // 自定义快捷键 + selectKey.value = 'custom'; + customKey.value = hotkey; + } + + /** + * 保存配置 + */ + function setConfig() { + const config = { + hotkey: null, + printDetails: printDetails.value, + printFolding: printFolding.value, + autoCheckUpdate: autoCheckUpdate.value, + }; + if (selectKey.value === 'custom') { + // 自定义输入是否有效 + if (customKey.value === '') { + EditorRendererKit.print('warn', t('custom-key-error')); + return; + } + // 不可以使用双引号(避免 json 值中出现双引号而解析错误,导致插件加载失败) + if (customKey.value.includes('"')) { + customKey.value = customKey.value.replace(/\"/g, ''); + EditorRendererKit.print('warn', t('quote-error')); + return; + } + config.hotkey = customKey.value; + } else { + config.hotkey = selectKey.value; + } + // 保存到本地 + ConfigManager.set(config); + } + + /** + * 应用按钮点击回调 + * @param {*} event + */ + function onApplyBtnClick(event) { + // 保存配置 + setConfig(); + } + + /** + * 翻译 + * @param {string} key + */ + function t(key) { + return I18n.get(LANG, key); + } + + /** + * 生命周期:挂载后 + */ + onMounted(() => { + // 获取配置 + getConfig(); + // 覆盖 a 标签点击回调(使用默认浏览器打开网页) + const links = document.querySelectorAll('a[href]'); + links.forEach((link) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + const url = link.getAttribute('href'); + shell.openExternal(url); + }); + }); + // (主进程)检查更新 + RendererEvent.send('check-update', false); + }); + + /** + * 生命周期:卸载前 + */ + onBeforeUnmount(() => { + + }); + + return { + presets, + selectKey, + customKey, + printDetails, + printFolding, + autoCheckUpdate, + repositoryUrl, + packageName, + onApplyBtnClick, + t, + }; + + }, + +}; + +// 创建实例 +const app = createApp(App); +// 挂载 +app.mount('#app');