代码整理
254
src/core/types.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { Chunk } from "../scripts/terminal";
|
||||
import { FrameDetails, Info, NodeInfoData, TreeData } from "../views/devtools/data";
|
||||
|
||||
export enum Page {
|
||||
None = "None",
|
||||
Inject = "Inject",
|
||||
Devtools = "Devtools",
|
||||
Background = "Background",
|
||||
Content = "Content",
|
||||
Popup = "Popup",
|
||||
Options = "Options",
|
||||
}
|
||||
|
||||
// #region 定义接受发送的数据声明,方便定位
|
||||
export interface RequestTreeInfoData {
|
||||
/**
|
||||
* 当前正在使用的frameID
|
||||
*/
|
||||
frameID: number;
|
||||
}
|
||||
export type ResponseTreeInfoData = TreeData;
|
||||
|
||||
export interface RequestNodeInfoData {
|
||||
/**
|
||||
* 节点的UUID
|
||||
*/
|
||||
uuid: string;
|
||||
}
|
||||
export type ResponseNodeInfoData = NodeInfoData;
|
||||
|
||||
export interface RequestSupportData {}
|
||||
export interface ResponseSupportData {
|
||||
/**
|
||||
* 是否支持
|
||||
*/
|
||||
support: boolean;
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
msg: string;
|
||||
/**
|
||||
* engine版本
|
||||
*/
|
||||
version: string;
|
||||
}
|
||||
export class DynamicAtlas {
|
||||
/**
|
||||
* 是否启用动态图集
|
||||
*/
|
||||
enable: boolean = false;
|
||||
atlasCount: number = 0;
|
||||
maxAtlasCount: number = 0;
|
||||
maxFrameSize: number = 0;
|
||||
textureSize: number = 0;
|
||||
textureBleeding: boolean = true;
|
||||
/**
|
||||
* 是否支持在游戏中查看
|
||||
*/
|
||||
supportView: boolean = false;
|
||||
}
|
||||
export class ResponseGameInfoData {
|
||||
public dynamicAtals = new DynamicAtlas();
|
||||
}
|
||||
export type ResponseUpdateFramesData = FrameDetails[];
|
||||
|
||||
export interface RequestUseFrameData {
|
||||
id: number;
|
||||
}
|
||||
export interface ResponseUseFrameData {
|
||||
id: number;
|
||||
}
|
||||
export type RequestSetPropertyData = Info;
|
||||
export type ResponseSetPropertyData = Info;
|
||||
export type RequestLogData = string[];
|
||||
export type ResponseErrorData = string;
|
||||
export enum Msg {
|
||||
None = "None",
|
||||
/**
|
||||
* 具体的节点信息
|
||||
*/
|
||||
RequestNodeInfo = "request-node-info",
|
||||
ResponseNodeInfo = "response-node-info",
|
||||
/**
|
||||
* 节点树信息
|
||||
*/
|
||||
RequstTreeInfo = "request-tree-info",
|
||||
ResponseTreeInfo = "response-tree-info",
|
||||
/**
|
||||
* 游戏支持信息
|
||||
*/
|
||||
RequestSupport = "request-support",
|
||||
ResponseSupport = "response-support",
|
||||
|
||||
ResponseMemoryInfo = "response-memory-info",
|
||||
VisibleFPS = "visible-fps",
|
||||
/**
|
||||
* 当前页面信息
|
||||
*/
|
||||
TabsInfo = "tabs_info",
|
||||
/**
|
||||
* 获取页面ID
|
||||
*/
|
||||
GetTabID = "GetTabID",
|
||||
/**
|
||||
* 用户主动选中的节点
|
||||
*/
|
||||
InspectNode = "inspect-node",
|
||||
/**
|
||||
* 鼠标滑过节点
|
||||
*/
|
||||
HoverNode = "hover-node",
|
||||
/**
|
||||
* 选中节点
|
||||
*/
|
||||
SelectNode = "select-node",
|
||||
/**
|
||||
* 更新页面的frame
|
||||
*/
|
||||
ResponseUpdateFrames = "response-update-frames",
|
||||
RequestUseFrame = "request-use-frame",
|
||||
ResponseUseFrame = "response-use-frame",
|
||||
|
||||
RequestLogData = "request-log-data",
|
||||
RequestLogCustom = "request-log-custom",
|
||||
ReqWriteClipboard = "request-write-clipboard",
|
||||
RequestSetProperty = "request-set-property",
|
||||
ResponseSetProperty = "response-set-property",
|
||||
RequestVisible = "request-visible",
|
||||
RequestDestroy = "request-destroy",
|
||||
|
||||
ResponseError = "response-error",
|
||||
|
||||
RequestGameInfo = "request-game-info",
|
||||
ResponseGameInfo = "response-game-info",
|
||||
|
||||
RequestDynamicAtlasView = "request-dynamic-atlas-view",
|
||||
ResponseDynamicAtlasView = "response-dynamic-atlas-view",
|
||||
}
|
||||
|
||||
export class PluginEvent {
|
||||
public static FLAG = "cc-inspector";
|
||||
/**
|
||||
* 增加一个消息的标记位,方便知道是自己插件的消息
|
||||
*/
|
||||
flag: string = PluginEvent.FLAG;
|
||||
/**
|
||||
* 消息是否有效
|
||||
*/
|
||||
valid: boolean = false;
|
||||
/**
|
||||
* 消息的类型
|
||||
*/
|
||||
msg: Msg | null = null;
|
||||
/**
|
||||
* 携带的数据
|
||||
*/
|
||||
data: any = null;
|
||||
|
||||
/**
|
||||
* 事件发送的源头
|
||||
*/
|
||||
source: Page | null = null;
|
||||
/**
|
||||
* 事件要发送的目标
|
||||
*/
|
||||
target: Page | null = null;
|
||||
isTargetDevtools() {
|
||||
return this.target === Page.Devtools;
|
||||
}
|
||||
isTargetBackground() {
|
||||
return this.target === Page.Background;
|
||||
}
|
||||
isTargetContent() {
|
||||
return this.target === Page.Content;
|
||||
}
|
||||
/**
|
||||
* 将addListener监听的数据转换为类
|
||||
*/
|
||||
static create(data: any): PluginEvent {
|
||||
let obj = data;
|
||||
if (typeof data === "string") {
|
||||
obj = JSON.stringify(data);
|
||||
} else if (typeof data === "object") {
|
||||
obj = data;
|
||||
} else {
|
||||
debugger;
|
||||
}
|
||||
|
||||
const ret = new PluginEvent(Page.None, Page.None, Msg.None, null);
|
||||
if (obj.flag !== PluginEvent.FLAG) {
|
||||
ret.valid = false;
|
||||
} else {
|
||||
const cls = data as PluginEvent;
|
||||
ret.source = cls.source;
|
||||
ret.target = cls.target;
|
||||
ret.msg = cls.msg;
|
||||
ret.data = cls.data;
|
||||
ret.valid = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
check(source: Page, target: Page) {
|
||||
return source && target && this.source === source && this.target === target;
|
||||
}
|
||||
|
||||
reset(source: Page | null, target: Page | null) {
|
||||
if (source && target) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
static finish(event: PluginEvent) {
|
||||
event.source = event.target = null;
|
||||
}
|
||||
toChunk(): Chunk[] {
|
||||
return [new Chunk(new Date().toLocaleString()).color("white").background("black").padding("0 4px"), new Chunk(this.source).color("white").background("red").padding("0 4px").margin("0 0 0 5px"), new Chunk("=>").color("black").background("yellow").bold(), new Chunk(this.target, false).color("white").background("green").padding("0 4px"), new Chunk(this.msg, true).color("white").background("black").margin("0 0 0 5px").padding("0 6px"), new Chunk(JSON.stringify(this.data))];
|
||||
}
|
||||
constructor(source: Page, target: Page, msg: Msg, data?: any) {
|
||||
if (PageInclude(target)) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
} else {
|
||||
console.warn(`无效的target: ${target}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inEnum(enumValues: any, value: Page | Msg) {
|
||||
for (let key in enumValues) {
|
||||
if (enumValues.hasOwnProperty(key)) {
|
||||
//@ts-ignore
|
||||
let itemEnum = enumValues[key] as string;
|
||||
if (itemEnum === value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function PageInclude(page: Page) {
|
||||
return inEnum(Page, page);
|
||||
}
|
||||
|
||||
export function MsgInclude(msg: Msg) {
|
||||
return inEnum(Msg, msg);
|
||||
}
|
||||
export const debugLog: boolean = false;
|
||||
38
src/core/util.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Page } from "./types";
|
||||
|
||||
interface LogOptions {
|
||||
data: any;
|
||||
flag?: string;
|
||||
color?: "red" | "blue";
|
||||
}
|
||||
|
||||
export function log(options: LogOptions) {
|
||||
const data: any = options.data;
|
||||
const time = new Date().toLocaleString();
|
||||
let log = "";
|
||||
if (typeof data === "string") {
|
||||
log = data;
|
||||
} else if (typeof data === "object") {
|
||||
log = JSON.stringify(data);
|
||||
}
|
||||
|
||||
let str = "";
|
||||
if (options.flag) {
|
||||
str = `[${time}][${options.flag}]: ${log} `;
|
||||
} else {
|
||||
str = `[${time}]: ${log} `;
|
||||
}
|
||||
if (options.color) {
|
||||
console.log(`%c${str}`, `color:${options.color};`);
|
||||
} else {
|
||||
console.log(str);
|
||||
}
|
||||
}
|
||||
|
||||
export function assembleDevToolsName(id: number) {
|
||||
return `${Page.Devtools}-${id}`;
|
||||
}
|
||||
export function getDevToolsInspectorId(name: string) {
|
||||
const id = name.split("-")[1];
|
||||
return id ? Number(id) : 0;
|
||||
}
|
||||
80
src/ga/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { GA_Button, GA_EventName, MeasurementBody } from "./type";
|
||||
|
||||
const API_SECRET = "_yU7eNTgT4Khe2Jo22Ki_g";
|
||||
const MEASUREMENT_ID = "G-RW7J0JZ6T5";
|
||||
const GA_ENDPOINT = "https://www.google-analytics.com/mp/collect";
|
||||
|
||||
export class GoogleAnalytics {
|
||||
async getOrCreateSessionId() {
|
||||
const result = await chrome.storage.local.get("clientId");
|
||||
let clientId = result.clientId;
|
||||
if (!clientId) {
|
||||
clientId = self.crypto.randomUUID();
|
||||
await chrome.storage.local.set({ clientId });
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
private isChromeEnv() {
|
||||
return !!chrome?.storage?.local?.get;
|
||||
}
|
||||
public async fireEventWithParam(name: GA_EventName, param: string) {
|
||||
if (!this.isChromeEnv()) {
|
||||
return;
|
||||
}
|
||||
const time = Date.now();
|
||||
const id = await this.getOrCreateSessionId();
|
||||
fetch(`${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
client_id: id,
|
||||
events: [
|
||||
{
|
||||
name: name,
|
||||
params: {
|
||||
id: param,
|
||||
session_id: time.toString(),
|
||||
engagement_time_msec: time.toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
} as MeasurementBody),
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
public async fireEvent(name: string) {
|
||||
if (!this.isChromeEnv()) {
|
||||
return;
|
||||
}
|
||||
const time = Date.now();
|
||||
const id = await this.getOrCreateSessionId();
|
||||
fetch(`${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
client_id: id,
|
||||
events: [
|
||||
{
|
||||
name: name,
|
||||
params: {
|
||||
session_id: time.toString(),
|
||||
engagement_time_msec: time.toString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
} as MeasurementBody),
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
async clickButton(btn: GA_Button) {
|
||||
await this.fireEventWithParam(GA_EventName.ButtonClicked, btn);
|
||||
}
|
||||
async openView(view: string) {
|
||||
await this.fireEventWithParam(GA_EventName.PageView, view);
|
||||
}
|
||||
}
|
||||
export const ga = new GoogleAnalytics();
|
||||
111
src/ga/type.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 发送的消息数据结构
|
||||
*
|
||||
* @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#payload_post_body
|
||||
* @github https://github.dev/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/scripts/google-analytics.js#L69
|
||||
*/
|
||||
export interface MeasurementBody {
|
||||
/**
|
||||
* 用户的ID,用于标识用户
|
||||
*/
|
||||
client_id: string;
|
||||
/**
|
||||
* 用户的唯一标识,只能包含 utf-8 字符。
|
||||
*/
|
||||
user_id?: string;
|
||||
/**
|
||||
* 事件相关联的时间的 UNIX 时间戳,此值应仅设置为记录过去发生的事件。
|
||||
*/
|
||||
timestamp_micros?: number;
|
||||
/**
|
||||
* 用户属性用于描述用户群细分,例如语言偏好设置或地理位置。
|
||||
*
|
||||
* @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?hl=zh-cn&client_type=gtag
|
||||
*/
|
||||
user_properties?: Object;
|
||||
/**
|
||||
* 用户提供的数据。
|
||||
*
|
||||
*@doc https://developers.google.com/analytics/devguides/collection/ga4/uid-data?hl=zh-cn
|
||||
*/
|
||||
user_data?: Object;
|
||||
/**
|
||||
* 设置请求的用户意见征求设置。
|
||||
* @doc https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#payload_consent
|
||||
*/
|
||||
consent?: Object;
|
||||
/**
|
||||
* 每个请求最多可以发送 25 个事件
|
||||
*/
|
||||
events?: MeasurementEvent[];
|
||||
}
|
||||
export interface MeasurementEvent {
|
||||
/**
|
||||
* 事件的名称。
|
||||
*
|
||||
* Google提供的事件: https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events?hl=zh-cn#add_payment_info
|
||||
* 预留的事件名:https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#reserved_event_names
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 预留的参数名:https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?hl=zh-cn&client_type=gtag#reserved_parameter_names
|
||||
*/
|
||||
params?: {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 在实时报告中查看事件,需要该参数
|
||||
*/
|
||||
session_id?: string;
|
||||
/**
|
||||
* 事件的互动时长(以毫秒为单位)
|
||||
*/
|
||||
engagement_time_msec?: string;
|
||||
};
|
||||
}
|
||||
export interface GA_Event_PageView extends MeasurementEvent {
|
||||
name: "page_view";
|
||||
params: {
|
||||
page_title: string;
|
||||
page_location: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum GA_EventName {
|
||||
ButtonClicked = "button_clicked",
|
||||
PageView = "page_view",
|
||||
SpaceVisible = "space_visible",
|
||||
MouseMenu = "mouse_menu",
|
||||
Hierarchy = "hierarchy",
|
||||
Inspector = "Inspector",
|
||||
EngineVersion = "engine_version",
|
||||
AppVersion = "app_version",
|
||||
GamePlayer = "game_player",
|
||||
GamePause = "game_pause",
|
||||
GameStep = "game_step",
|
||||
TreeSearch = "tree-search",
|
||||
/**
|
||||
* 用户点击store广告链接
|
||||
*/
|
||||
ClickPluginLink = "click_plugin_link",
|
||||
/**
|
||||
* 用户主动关闭store广告
|
||||
*/
|
||||
CloseAd = "close_ad",
|
||||
/**
|
||||
* 展示广告
|
||||
*/
|
||||
ShowAd = "show_ad",
|
||||
/**
|
||||
* 用户主动使用inspector检查游戏节点
|
||||
*/
|
||||
GameInspector = "game_inspector",
|
||||
}
|
||||
export enum GA_Button {
|
||||
Github = "github",
|
||||
Issues = "issues",
|
||||
QQ = "qq",
|
||||
/**
|
||||
* 当页面不支持cocos时,用户手动点击了刷新
|
||||
*/
|
||||
FreshManual = "fresh-manual",
|
||||
}
|
||||
1
src/i18n/en.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const title = "CCInspector";
|
||||
1
src/i18n/zh.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const title = "CCInspector";
|
||||
17
src/main.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import CCP from "cc-plugin/src/ccp/entry-main";
|
||||
import { BuilderOptions } from "cc-plugin/src/declare";
|
||||
import pluginConfig from "../cc-plugin.config";
|
||||
|
||||
CCP.init(pluginConfig, {
|
||||
load: () => {
|
||||
console.log("plugin load");
|
||||
},
|
||||
builder: {
|
||||
onAfterBuild(target: BuilderOptions) {},
|
||||
},
|
||||
messages: {
|
||||
showPanel() {
|
||||
CCP.Adaptation.Panel.open("self.main");
|
||||
},
|
||||
},
|
||||
});
|
||||
22
src/panel/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { loadSlim } from "@tsparticles/slim";
|
||||
import Particles from "@tsparticles/vue3";
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { createApp } from "vue";
|
||||
import pluginConfig from "../../cc-plugin.config";
|
||||
import App from "./index.vue";
|
||||
export default CCP.init(pluginConfig, {
|
||||
ready: function (rootElement: any, args: any) {
|
||||
const app = createApp(App);
|
||||
app.use(ccui);
|
||||
//@ts-ignore
|
||||
app.use(Particles, {
|
||||
init: async (engine) => {
|
||||
await loadSlim(engine);
|
||||
},
|
||||
});
|
||||
app.mount(rootElement);
|
||||
},
|
||||
});
|
||||
255
src/panel/index.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="panel ccui-scrollbar">
|
||||
<vue-particles id="tsparticles" :options="options"></vue-particles>
|
||||
<div class="head">
|
||||
<img class="icon" src="../../doc/icon128.png" />
|
||||
<span class="txt">Cocos Inspector</span>
|
||||
</div>
|
||||
<div class="content" :class="horizontal ? 'content-row' : 'content-col'">
|
||||
<div class="title">
|
||||
<p style="font-size: 40px; font-weight: bold; color: white">为Coocs游戏开发加速。</p>
|
||||
<div style="font-size: 18px; font-weight: normal; margin: 30px 0; line-height: 40px; color: white">在浏览器中查看节点树、节点属性。<br />支持Creator所有版本。</div>
|
||||
<div class="link">
|
||||
<CCButton color="rgb(38,187,255)" class="download" @click="onClickChrome">
|
||||
<div class="list">
|
||||
<img src="./res/chrome.png" />
|
||||
<div class="txt">下载</div>
|
||||
</div>
|
||||
</CCButton>
|
||||
<CCButton style="margin-left: 20px" color="rgb(38,187,255)" class="download" @click="onClickEdge">
|
||||
<div class="list">
|
||||
<img src="./res/edge.png" />
|
||||
<div class="txt">下载</div>
|
||||
</div>
|
||||
</CCButton>
|
||||
<i @click="onClickGithub" class="iconfont icon_github github"></i>
|
||||
</div>
|
||||
</div>
|
||||
<iframe class="video" src="//player.bilibili.com/player.html?isOutside=true&aid=113803849106700&bvid=BV1jzcHeSEh3&cid=27797426092&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>
|
||||
<!-- <video class="video" controls="true" autoplay loop muted src="https://www.bilibili.com/video/BV1jzcHeSEh3/"></video> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, onMounted, ref } from "vue";
|
||||
import PluginConfig from "../../cc-plugin.config";
|
||||
const { CCInput, CCButton } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "index",
|
||||
components: { CCButton },
|
||||
setup(props, { emit }) {
|
||||
onMounted(() => {
|
||||
updateLayout();
|
||||
});
|
||||
const horizontal = ref(true);
|
||||
function updateLayout() {
|
||||
const w = window.document.body.clientWidth;
|
||||
horizontal.value = w > 1100;
|
||||
}
|
||||
window.addEventListener("resize", updateLayout);
|
||||
const msg = ref(PluginConfig.manifest.name);
|
||||
const count = ref(0);
|
||||
const options = ref({
|
||||
background: {
|
||||
color: {
|
||||
value: "#000", // 粒子颜色
|
||||
},
|
||||
},
|
||||
fpsLimit: 60,
|
||||
interactivity: {
|
||||
events: {
|
||||
onClick: {
|
||||
enable: true,
|
||||
mode: "push", // 可用的click模式有: "push", "remove", "repulse", "bubble"。
|
||||
},
|
||||
onHover: {
|
||||
enable: true,
|
||||
mode: "grab", // 可用的hover模式有: "grab", "repulse", "bubble"。
|
||||
},
|
||||
resize: true,
|
||||
},
|
||||
modes: {
|
||||
bubble: {
|
||||
distance: 400,
|
||||
duration: 2,
|
||||
opacity: 0.8,
|
||||
size: 40,
|
||||
},
|
||||
push: {
|
||||
quantity: 4,
|
||||
},
|
||||
repulse: {
|
||||
distance: 200,
|
||||
duration: 0.4,
|
||||
},
|
||||
},
|
||||
},
|
||||
particles: {
|
||||
color: {
|
||||
value: "#ffffff",
|
||||
},
|
||||
links: {
|
||||
color: "#ffffff", // '#dedede'。线条颜色。
|
||||
distance: 150, // 线条长度
|
||||
enable: true, // 是否有线条
|
||||
opacity: 0.5, // 线条透明度。
|
||||
width: 1, // 线条宽度。
|
||||
},
|
||||
collisions: {
|
||||
enable: false,
|
||||
},
|
||||
move: {
|
||||
direction: "none",
|
||||
enable: true,
|
||||
outMode: "bounce",
|
||||
random: false,
|
||||
speed: 4, // 粒子运动速度。
|
||||
straight: false,
|
||||
},
|
||||
number: {
|
||||
density: {
|
||||
enable: true,
|
||||
area: 800,
|
||||
},
|
||||
value: 80, // 粒子数量。
|
||||
},
|
||||
opacity: {
|
||||
value: 0.5, // 粒子透明度。
|
||||
},
|
||||
shape: {
|
||||
type: "circle", // 可用的粒子外观类型有:"circle","edge","triangle", "polygon","star"
|
||||
},
|
||||
size: {
|
||||
random: true,
|
||||
value: 5,
|
||||
},
|
||||
},
|
||||
detectRetina: true,
|
||||
});
|
||||
return {
|
||||
options,
|
||||
horizontal,
|
||||
msg,
|
||||
count,
|
||||
onClickChrome() {
|
||||
const url = "https://chromewebstore.google.com/detail/cc-inspector/hejbkamkfnkifppoaljcidepkhgaahcj?hl=zh-CN&utm_source=ext_sidebar";
|
||||
window.open(url);
|
||||
},
|
||||
onClickEdge() {
|
||||
const url = "https://microsoftedge.microsoft.com/addons/detail/ccinspector/jcghedjiinobccoffphdgkplcildapgb";
|
||||
window.open(url);
|
||||
},
|
||||
onClickGithub() {
|
||||
const url = "https://github.com/tidys/cc-inspector-chrome";
|
||||
window.open(url);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 30px;
|
||||
box-sizing: border-box;
|
||||
background-color: black;
|
||||
.head {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
.icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.txt {
|
||||
color: #188ee1;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.content-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content-col {
|
||||
flex-direction: column;
|
||||
.video {
|
||||
margin-top: 40px !important;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
margin-top: 10px;
|
||||
margin-left: 20px;
|
||||
min-width: 450px;
|
||||
box-sizing: border-box;
|
||||
.link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
.github {
|
||||
cursor: pointer;
|
||||
padding: 0 20px;
|
||||
font-size: 30px;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
color: #188ee1;
|
||||
}
|
||||
&:active {
|
||||
color: rgb(255, 153, 0);
|
||||
}
|
||||
}
|
||||
.download {
|
||||
width: 160px;
|
||||
height: 60px;
|
||||
font-size: 20px !important;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.txt {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
line-height: 22px;
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.video {
|
||||
flex: 1;
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
min-height: 440px;
|
||||
margin: 5px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
border: 1px solid rgb(112, 112, 112);
|
||||
border-radius: 20px;
|
||||
box-shadow: 1px 1px 8px 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
src/panel/res/chrome.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/panel/res/edge.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
79
src/scripts/background/content.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { debugLog, Page, PluginEvent } from "../../core/types";
|
||||
import { FrameDetails } from "../../views/devtools/data";
|
||||
import { Terminal } from "../terminal";
|
||||
import { TabInfo } from "./tabInfo";
|
||||
|
||||
export class Content {
|
||||
public frameID: number = 0;
|
||||
/**
|
||||
* port的名字标识
|
||||
*/
|
||||
public name: string = Page.None;
|
||||
/**
|
||||
* tab.id作为唯一标识
|
||||
*/
|
||||
public tabID: number | null = null;
|
||||
public title: string = "";
|
||||
public url: string = "";
|
||||
protected port: chrome.runtime.Port | null = null;
|
||||
public tab: chrome.tabs.Tab | null = null;
|
||||
public terminal: Terminal = null;
|
||||
/**
|
||||
* 是否正在使用
|
||||
*/
|
||||
public using: boolean = false;
|
||||
private tabInfo: TabInfo | null = null;
|
||||
constructor(tab: chrome.tabs.Tab, port: chrome.runtime.Port, tabInfo: TabInfo) {
|
||||
this.tabInfo = tabInfo;
|
||||
this.port = port;
|
||||
this.tab = tab;
|
||||
this.name = port.name;
|
||||
this.tabID = tab.id;
|
||||
this.url = port.sender.url;
|
||||
this.title = tab.title;
|
||||
this.terminal = new Terminal(`Port-${this.name}`);
|
||||
port.onMessage.addListener((data: any, port: chrome.runtime.Port) => {
|
||||
const event = PluginEvent.create(data);
|
||||
debugLog && console.log(...this.terminal.chunkMessage(event.toChunk()));
|
||||
if (event.valid && this.onMessage) {
|
||||
this.onMessage(event);
|
||||
} else {
|
||||
debugLog && console.log(...this.terminal.log(JSON.stringify(data)));
|
||||
}
|
||||
});
|
||||
port.onDisconnect.addListener((port: chrome.runtime.Port) => {
|
||||
debugLog && console.log(...this.terminal.disconnect(""));
|
||||
this.onDisconnect(port);
|
||||
});
|
||||
this.frameID = port.sender.frameId || 0;
|
||||
}
|
||||
getFrameDetais(): FrameDetails {
|
||||
return {
|
||||
tabID: this.tabID,
|
||||
url: this.url,
|
||||
frameID: this.frameID,
|
||||
};
|
||||
}
|
||||
private onDisconnect(disPort: chrome.runtime.Port) {
|
||||
this.tabInfo.removePort(this);
|
||||
}
|
||||
|
||||
public onMessage(data: PluginEvent) {
|
||||
// content的数据一般都是要同步到devtools
|
||||
if (data.isTargetDevtools()) {
|
||||
if (this.tabInfo.devtool) {
|
||||
this.tabInfo.devtool.send(data);
|
||||
} else {
|
||||
debugger;
|
||||
}
|
||||
} else {
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
|
||||
send(data: PluginEvent) {
|
||||
if (this.port) {
|
||||
this.port.postMessage(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/scripts/background/devtools.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { debugLog, Msg, Page, PluginEvent, RequestUseFrameData, ResponseSupportData } from "../../core/types";
|
||||
import { Terminal } from "../terminal";
|
||||
import { TabInfo } from "./tabInfo";
|
||||
|
||||
export class Devtools {
|
||||
/**
|
||||
* port的名字标识
|
||||
*/
|
||||
public name: string = Page.None;
|
||||
/**
|
||||
* tab.id作为唯一标识
|
||||
*/
|
||||
public tabID: number | null = null;
|
||||
public title: string = "";
|
||||
public url: string = "";
|
||||
protected port: chrome.runtime.Port | null = null;
|
||||
public tab: chrome.tabs.Tab | null = null;
|
||||
|
||||
public terminal: Terminal = null;
|
||||
public tabInfo: TabInfo | null = null;
|
||||
constructor(port: chrome.runtime.Port, tabInfo: TabInfo) {
|
||||
this.tabInfo = tabInfo;
|
||||
this.port = port;
|
||||
this.name = port.name;
|
||||
this.url = port.sender.url;
|
||||
this.terminal = new Terminal(`Port-${this.name}`);
|
||||
port.onMessage.addListener((data: any, port: chrome.runtime.Port) => {
|
||||
const event = PluginEvent.create(data);
|
||||
debugLog && console.log(...this.terminal.chunkMessage(event.toChunk()));
|
||||
if (event.valid && this.onMessage) {
|
||||
this.onMessage(event);
|
||||
} else {
|
||||
debugLog && console.log(...this.terminal.log(JSON.stringify(data)));
|
||||
}
|
||||
});
|
||||
port.onDisconnect.addListener((port: chrome.runtime.Port) => {
|
||||
debugLog && console.log(...this.terminal.disconnect(""));
|
||||
if (this.onDisconnect) {
|
||||
this.onDisconnect(port);
|
||||
}
|
||||
});
|
||||
}
|
||||
public onDisconnect(port: chrome.runtime.Port) {
|
||||
this.tabInfo.removeDevtools(this);
|
||||
}
|
||||
public onMessage(data: PluginEvent) {
|
||||
if (data.msg === Msg.RequestUseFrame) {
|
||||
// 因为devtool是定时器驱动,这里改变了content,后续就会将数据派发到对应的content中去
|
||||
this.tabInfo.useFrame((data.data as RequestUseFrameData).id);
|
||||
} else {
|
||||
// 从devtools过来的消息统一派发到目标content中
|
||||
if (data.check(Page.Devtools, Page.Background)) {
|
||||
data.reset(Page.Background, Page.Content);
|
||||
this.tabInfo.sendMsgToContent(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
send(data: PluginEvent) {
|
||||
if (this.port) {
|
||||
this.port.postMessage(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/scripts/background/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { debugLog, Page, PluginEvent } from "../../core/types";
|
||||
import { getDevToolsInspectorId } from "../../core/util";
|
||||
import { Terminal } from "../terminal";
|
||||
import { tabMgr } from "./tabMgr";
|
||||
const terminal = new Terminal(Page.Background);
|
||||
debugLog && console.log(...terminal.init());
|
||||
|
||||
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
|
||||
if (port.name === Page.Content) {
|
||||
const tab: chrome.tabs.Tab | undefined = port.sender?.tab;
|
||||
const tabID = tab.id;
|
||||
if (tabID === undefined || tabID <= 0) {
|
||||
return;
|
||||
}
|
||||
tabMgr.addTab(tab, port);
|
||||
} else if (port.name.startsWith(Page.Devtools)) {
|
||||
const id = getDevToolsInspectorId(port.name);
|
||||
const tab = tabMgr.findTab(id);
|
||||
if (tab) {
|
||||
tab.addDevtools(port);
|
||||
} else {
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
});
|
||||
chrome.runtime.onMessage.addListener((request: PluginEvent, sender: any, sendResponse: any) => {
|
||||
const event = PluginEvent.create(request);
|
||||
const tabID = sender.tab.id;
|
||||
const tabInfo = tabMgr.findTab(tabID);
|
||||
if (tabInfo) {
|
||||
if (event.check(Page.Content, Page.Background)) {
|
||||
// 监听来自content.js发来的事件,将消息转发到devtools
|
||||
event.reset(Page.Background, Page.Devtools);
|
||||
console.log(`%c[Message]url:${sender.url}]\n${JSON.stringify(request)}`, "color:green");
|
||||
if (tabInfo.devtool) {
|
||||
tabInfo.devtool.send(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
chrome.tabs.onActivated.addListener(({ tabId, windowId }) => {});
|
||||
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
||||
//
|
||||
});
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
// 页面发生刷新,通知重新生成数据
|
||||
if (changeInfo.status === "complete") {
|
||||
const { id } = tab;
|
||||
// -1为自己
|
||||
if (id >= 0) {
|
||||
const tabInfo = tabMgr.findTab(id);
|
||||
if (tabInfo) {
|
||||
tabInfo.useFrame(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
80
src/scripts/background/tabInfo.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Msg, Page, PluginEvent, ResponseUpdateFramesData, ResponseUseFrameData } from "../../core/types";
|
||||
import { FrameDetails } from "../../views/devtools/data";
|
||||
import { Content } from "./content";
|
||||
import { Devtools } from "./devtools";
|
||||
|
||||
export class TabInfo {
|
||||
/**
|
||||
* 标签的ID
|
||||
*/
|
||||
public tabID: number;
|
||||
constructor(tabID: number) {
|
||||
this.tabID = tabID;
|
||||
}
|
||||
/**
|
||||
* 因为iframe的原因,可能对应多个,主iframe的id是0
|
||||
*/
|
||||
public contentArray: Array<Content> = [];
|
||||
addContent(tab: chrome.tabs.Tab, port: chrome.runtime.Port) {
|
||||
// 新的content连上来,需要更新devtools
|
||||
let portContent: Content = new Content(tab, port, this);
|
||||
this.contentArray.push(portContent);
|
||||
this.updateFrames();
|
||||
}
|
||||
public removePort(item: Content) {
|
||||
let index = this.contentArray.findIndex((el) => el === item);
|
||||
if (index > -1) {
|
||||
this.contentArray.splice(index, 1);
|
||||
this.updateFrames();
|
||||
|
||||
// 使用第一个frame
|
||||
if (this.contentArray.length) {
|
||||
const id = this.contentArray[0].frameID;
|
||||
this.useFrame(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
public removeDevtools(item: Devtools) {
|
||||
this.devtool = null;
|
||||
}
|
||||
useFrame(id: number) {
|
||||
this.contentArray.map((content) => {
|
||||
content.using = content.frameID === id;
|
||||
});
|
||||
this.sendMsgToDevtool(Msg.ResponseUseFrame, { id } as ResponseUseFrameData);
|
||||
}
|
||||
/**
|
||||
* 通知devtools更新
|
||||
*/
|
||||
private updateFrames() {
|
||||
const data: FrameDetails[] = [];
|
||||
this.contentArray.forEach((item) => {
|
||||
const frame = (item as Content).getFrameDetais();
|
||||
data.push(frame);
|
||||
});
|
||||
this.sendMsgToDevtool(Msg.ResponseUpdateFrames, data as ResponseUpdateFramesData);
|
||||
}
|
||||
private sendMsgToDevtool(msg: Msg, data: any) {
|
||||
if (this.devtool) {
|
||||
const event = new PluginEvent(Page.Background, Page.Devtools, msg, data);
|
||||
this.devtool.send(event);
|
||||
}
|
||||
}
|
||||
public sendMsgToContent(data: PluginEvent) {
|
||||
const content = this.contentArray.find((el) => el.using);
|
||||
if (content) {
|
||||
content.send(data);
|
||||
} else {
|
||||
// 当页面没有完成刷新状态时,conent并没有using,就会触发此处逻辑
|
||||
// 在页面完成刷新后,会主动设置为using
|
||||
}
|
||||
}
|
||||
public devtool: Devtools | null = null;
|
||||
addDevtools(port: chrome.runtime.Port) {
|
||||
if (this.devtool === null) {
|
||||
this.devtool = new Devtools(port, this);
|
||||
} else {
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/scripts/background/tabMgr.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { TabInfo } from "./tabInfo";
|
||||
|
||||
export class TabMgr {
|
||||
/**
|
||||
* chrome打开的所有标签页面
|
||||
*/
|
||||
public tabArray: TabInfo[] = [];
|
||||
public findTab(id: number): TabInfo | null {
|
||||
return this.tabArray.find((el) => el.tabID === id) || null;
|
||||
}
|
||||
|
||||
addTab(tab: chrome.tabs.Tab, port: chrome.runtime.Port) {
|
||||
let tabInfo = this.findTab(tab.id);
|
||||
if (!tabInfo) {
|
||||
tabInfo = new TabInfo(tab.id);
|
||||
this.tabArray.push(tabInfo);
|
||||
}
|
||||
tabInfo.addContent(tab, port);
|
||||
}
|
||||
}
|
||||
export const tabMgr = new TabMgr();
|
||||
22
src/scripts/const.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { GA_EventName } from "../ga/type";
|
||||
|
||||
export enum DocumentEvent {
|
||||
/**
|
||||
* 从inject到content的事件
|
||||
*/
|
||||
Inject2Content = "inject2content",
|
||||
/**
|
||||
* 从content到inject的事件
|
||||
*/
|
||||
Content2Inject = "content2inject",
|
||||
EngineVersion = "engineVersion",
|
||||
GoogleAnalytics = "googleAnalytics",
|
||||
LoadInjectCss = "load-inject-css",
|
||||
InspectorClear = "inspector_clear",
|
||||
GameInspectorBegan = "GameInspectorBegan",
|
||||
GameInspectorEnd = "GameInspectorEnd",
|
||||
}
|
||||
export interface GoogleAnalyticsData {
|
||||
event: GA_EventName;
|
||||
params: string;
|
||||
}
|
||||
85
src/scripts/content/index.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// content.js 和原始界面共享DOM,具有操作dom的能力
|
||||
// 但是不共享js,要想访问页面js,只能通过注入的方式
|
||||
import { ChromeConst } from "cc-plugin/src/chrome/const";
|
||||
import { debugLog, Msg, Page, PluginEvent } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { DocumentEvent, GoogleAnalyticsData } from "../const";
|
||||
import { Terminal } from "../terminal";
|
||||
|
||||
const terminal = new Terminal(Page.Content);
|
||||
debugLog && console.log(...terminal.init());
|
||||
|
||||
// #region 注入脚本
|
||||
export function injectScript(url: string) {
|
||||
if (chrome && chrome.runtime && chrome.runtime.getURL) {
|
||||
let content = chrome.runtime.getURL(url);
|
||||
const script = document.createElement("script");
|
||||
script.setAttribute("type", "text/javascript");
|
||||
script.setAttribute("src", content);
|
||||
script.onload = function () {
|
||||
// 加载注入脚本界面的css
|
||||
let css = chrome.runtime.getURL(ChromeConst.css.inject_view);
|
||||
const event = new CustomEvent(DocumentEvent.LoadInjectCss, { detail: [css] });
|
||||
document.dispatchEvent(event);
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
debugLog && console.log(...terminal.green(`inject script success: ${content}`));
|
||||
} else {
|
||||
debugLog && console.log(...terminal.red("inject script failed"));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener(DocumentEvent.EngineVersion, async (event: CustomEvent) => {
|
||||
const version: string = event.detail;
|
||||
if (version) {
|
||||
ga.fireEventWithParam(GA_EventName.EngineVersion, version);
|
||||
}
|
||||
});
|
||||
document.addEventListener(DocumentEvent.GoogleAnalytics, (event: CustomEvent) => {
|
||||
const data: GoogleAnalyticsData = event.detail;
|
||||
if (data && data.event) {
|
||||
if (data.params) {
|
||||
ga.fireEventWithParam(data.event, data.params);
|
||||
} else {
|
||||
ga.fireEvent(data.event);
|
||||
}
|
||||
}
|
||||
});
|
||||
// #region 和Inject通讯
|
||||
document.addEventListener(DocumentEvent.Inject2Content, (event: CustomEvent) => {
|
||||
let data: PluginEvent = PluginEvent.create(event.detail);
|
||||
if (data.valid && data.check(Page.Inject, Page.Content)) {
|
||||
debugLog && console.log(...terminal.chunkMessage(data.toChunk()));
|
||||
data.reset(Page.Content, Page.Devtools);
|
||||
if (connect) {
|
||||
// 接受来自inject.js的消息数据,然后中转到background.js
|
||||
connect.postMessage(data);
|
||||
} else {
|
||||
debugLog && console.log(...terminal.log(`connect is null`));
|
||||
console.log("connect is null");
|
||||
}
|
||||
} else {
|
||||
throw new Error(`invalid data: ${event.detail}`);
|
||||
}
|
||||
});
|
||||
// #region 和background通讯
|
||||
let connect: chrome.runtime.Port = chrome.runtime.connect({ name: Page.Content });
|
||||
connect.onDisconnect.addListener(() => {
|
||||
debugLog && console.log(...terminal.disconnect(""));
|
||||
connect = null;
|
||||
});
|
||||
connect.onMessage.addListener((data: PluginEvent, sender: chrome.runtime.Port) => {
|
||||
const event = PluginEvent.create(data);
|
||||
if (event.valid && event.check(Page.Background, Page.Content)) {
|
||||
debugLog && console.log(...terminal.chunkMessage(event.toChunk()));
|
||||
event.reset(Page.Content, Page.Inject);
|
||||
const e = new CustomEvent(DocumentEvent.Content2Inject, { detail: event });
|
||||
debugLog && console.log(...terminal.chunkSend(event.toChunk()));
|
||||
document.dispatchEvent(e);
|
||||
} else {
|
||||
throw new Error(`invalid data: ${data}`);
|
||||
}
|
||||
});
|
||||
injectScript(ChromeConst.script.inject);
|
||||
80
src/scripts/inject-view/ad.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div v-show="ads.length" class="ad">
|
||||
<div class="body" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave">
|
||||
<div class="list ccui-scrollbar">
|
||||
<Banner v-for="(item, index) in ads" :data="item" :key="index"></Banner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, onMounted, onUnmounted, ref, toRaw } from "vue";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import Banner from "./banner.vue";
|
||||
import { emitter, Msg } from "./const";
|
||||
import { AdItem, getAdData } from "./loader";
|
||||
import { ga } from "./util";
|
||||
const { CCButton } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "ad",
|
||||
components: { CCButton, Banner },
|
||||
setup(props, { emit }) {
|
||||
onMounted(async () => {
|
||||
const data = await getAdData();
|
||||
if (!data) {
|
||||
console.log(`get ad failed`);
|
||||
return;
|
||||
}
|
||||
if (!data.valid) {
|
||||
console.log(`set ad forbidden`);
|
||||
return;
|
||||
}
|
||||
if (!data.data.length) {
|
||||
console.log(`not find any ad`);
|
||||
return;
|
||||
}
|
||||
ads.value = data.data;
|
||||
console.log("get ads ", toRaw(ads.value));
|
||||
|
||||
ga(GA_EventName.ShowAd);
|
||||
});
|
||||
onUnmounted(() => {});
|
||||
function testBanner() {
|
||||
const data = new AdItem();
|
||||
data.name = "ad test 11111111111 11111111111 44444444444444 5555555555555 111111111111111111 2222222222222222 33333333333333 444444444444444";
|
||||
data.store = "http://www.baidu.com";
|
||||
emitter.emit(Msg.ChangeAd, data);
|
||||
}
|
||||
|
||||
let ads = ref<AdItem[]>([]);
|
||||
|
||||
return {
|
||||
ads,
|
||||
onMouseEnter() {},
|
||||
onMouseLeave() {},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@color-bg: #8d8d8da6;
|
||||
@color-hover: #f9c04e;
|
||||
@color-active: #ffaa00;
|
||||
.ad {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
.list {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
390
src/scripts/inject-view/app.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<template>
|
||||
<div class="ad" ref="rootEl" v-show="!picking" @contextmenu.prevent="onContextMenuRoot" @mouseleave="onMouseLeaveRoot" @mouseenter="onMouseEnterCocosLogo">
|
||||
<div class="title">
|
||||
<div class="btns" v-show="showBtns">
|
||||
<div v-for="(item, index) in listArray" :key="index" class="list" @click="item.click($event, item)" :title="item.txt" v-show="item.visible">
|
||||
<i class="iconfont icon" :class="item.icon" @contextmenu.prevent.stop="item.contextmenu"></i>
|
||||
</div>
|
||||
</div>
|
||||
<i class="iconfont icon_cocos cocos" @mousedown="onMouseDown" @click="onCocosLogoClick"></i>
|
||||
</div>
|
||||
<!-- <Memory></Memory> -->
|
||||
<CCDialog></CCDialog>
|
||||
<CCMenu></CCMenu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { IUiMenuItem } from "@xuyanfeng/cc-ui/types/cc-menu/const";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { defineComponent, onMounted, ref, toRaw } from "vue";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { DocumentEvent } from "../const";
|
||||
import { inspectTarget } from "../inject/inspect-list";
|
||||
import Ad from "./ad.vue";
|
||||
import Banner from "./banner.vue";
|
||||
import Memory from "./memory.vue";
|
||||
import { appStore } from "./store";
|
||||
import { ga } from "./util";
|
||||
declare const cc: any;
|
||||
const { CCDialog, CCMenu } = ccui.components;
|
||||
interface ListItem {
|
||||
icon: string;
|
||||
txt: string;
|
||||
visible: boolean;
|
||||
/**
|
||||
* 点击回调
|
||||
*/
|
||||
click: (event: MouseEvent, item: ListItem) => void;
|
||||
contextmenu: (event: MouseEvent) => void;
|
||||
}
|
||||
export default defineComponent({
|
||||
name: "ad",
|
||||
components: { CCDialog, Banner, Memory, CCMenu },
|
||||
setup() {
|
||||
function randomSupport(): { icon: string; title: string } {
|
||||
const arr = [
|
||||
{ icon: "icon_shop_cart", title: "冲冲冲" },
|
||||
{ icon: "icon_good", title: "赞一个" },
|
||||
{ icon: "icon_coffe", title: "请我喝杯咖啡" },
|
||||
];
|
||||
const idx = Math.floor(Math.random() * arr.length);
|
||||
return arr[idx];
|
||||
}
|
||||
|
||||
const store = appStore();
|
||||
store.init();
|
||||
const rnd = randomSupport();
|
||||
const { config } = storeToRefs(appStore());
|
||||
const listArray = ref<ListItem[]>([
|
||||
{
|
||||
icon: `${rnd.icon} ani_shop_cart`,
|
||||
txt: rnd.title,
|
||||
contextmenu: () => {},
|
||||
visible: true,
|
||||
click: () => {
|
||||
ccui.dialog.showDialog({
|
||||
title: "Recommended Plugins",
|
||||
comp: Ad,
|
||||
width: 310,
|
||||
height: 500,
|
||||
closeCB: () => {
|
||||
ga(GA_EventName.CloseAd);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "icon_do_play",
|
||||
click: (event: MouseEvent, item: ListItem) => {
|
||||
ga(GA_EventName.GamePlayer);
|
||||
if (typeof cc !== "undefined") {
|
||||
cc.game.resume();
|
||||
}
|
||||
},
|
||||
visible: true,
|
||||
txt: "game play",
|
||||
contextmenu: () => {},
|
||||
},
|
||||
{
|
||||
icon: "icon_do_pause",
|
||||
visible: true,
|
||||
txt: "game pause",
|
||||
click: () => {
|
||||
ga(GA_EventName.GamePause);
|
||||
if (typeof cc !== "undefined") {
|
||||
cc.game.pause();
|
||||
}
|
||||
},
|
||||
contextmenu: () => {},
|
||||
},
|
||||
{
|
||||
icon: "icon_do_step",
|
||||
visible: true,
|
||||
txt: "game step",
|
||||
click: () => {
|
||||
ga(GA_EventName.GameStep);
|
||||
if (typeof cc !== "undefined") {
|
||||
cc.game.step();
|
||||
}
|
||||
},
|
||||
contextmenu: () => {},
|
||||
},
|
||||
{
|
||||
icon: "icon_target",
|
||||
txt: "Inspect Game",
|
||||
visible: true,
|
||||
click: () => {
|
||||
ga(GA_EventName.GameInspector);
|
||||
if (config.value.autoHide) {
|
||||
showBtns.value = false;
|
||||
}
|
||||
picking.value = true;
|
||||
if (typeof cc === "undefined") {
|
||||
testInspector();
|
||||
} else {
|
||||
const event = new CustomEvent(DocumentEvent.GameInspectorBegan);
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
},
|
||||
contextmenu: (event: MouseEvent) => {
|
||||
const arr = [
|
||||
{ name: "Inspect Label", type: typeof cc !== "undefined" ? cc.Label : "cc.Label" }, //
|
||||
{ name: "Inspect Sprite", type: typeof cc !== "undefined" ? cc.Sprite : "cc.Sprite" },
|
||||
{ name: "Inspect Button", type: typeof cc !== "undefined" ? cc.Button : "cc.Button" },
|
||||
{ name: "Inspect RichText", type: typeof cc !== "undefined" ? cc.RichText : "cc.RichText" },
|
||||
];
|
||||
const compMenu: IUiMenuItem[] = arr.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
enabled: inspectTarget.enabled,
|
||||
selected: inspectTarget.isContainInspectType(item.type),
|
||||
callback: (menu: IUiMenuItem) => {
|
||||
ga(GA_EventName.MouseMenu, menu.name);
|
||||
if (menu.selected) {
|
||||
inspectTarget.removeInspectType(item.type);
|
||||
} else {
|
||||
inspectTarget.addInspectType(item.type);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
ccui.menu.showMenuByMouseEvent(event, [
|
||||
{
|
||||
name: "Clear",
|
||||
callback: (menu: IUiMenuItem) => {
|
||||
const event = new CustomEvent(DocumentEvent.InspectorClear);
|
||||
document.dispatchEvent(event);
|
||||
ga(GA_EventName.MouseMenu, menu.name);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Pick Top",
|
||||
selected: config.value.pickTop,
|
||||
callback: (menu: IUiMenuItem) => {
|
||||
config.value.pickTop = !config.value.pickTop;
|
||||
appStore().save();
|
||||
ga(GA_EventName.MouseMenu, menu.name);
|
||||
},
|
||||
},
|
||||
{ type: ccui.menu.MenuType.Separator },
|
||||
{
|
||||
name: "Filter Enabled",
|
||||
selected: inspectTarget.enabled,
|
||||
callback: (menu: IUiMenuItem) => {
|
||||
ga(GA_EventName.MouseMenu, menu.name);
|
||||
inspectTarget.enabled = !inspectTarget.enabled;
|
||||
},
|
||||
},
|
||||
...compMenu,
|
||||
]);
|
||||
},
|
||||
},
|
||||
]);
|
||||
document.addEventListener(DocumentEvent.GameInspectorEnd, () => {
|
||||
picking.value = false;
|
||||
});
|
||||
function testInspector() {
|
||||
const cursor = document.body.style.cursor;
|
||||
document.body.style.cursor = "zoom-in";
|
||||
function test(event: MouseEvent) {
|
||||
document.removeEventListener("mousedown", test, true);
|
||||
document.body.style.cursor = cursor;
|
||||
picking.value = false;
|
||||
}
|
||||
document.addEventListener("mousedown", test, true);
|
||||
}
|
||||
function recoverAssistantTop() {
|
||||
const top = toRaw(config.value.pos);
|
||||
updateAssistantTop(top);
|
||||
}
|
||||
|
||||
function updateAssistantTop(top: number) {
|
||||
const root = toRaw(rootEl.value) as HTMLDivElement;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
const maxTop = document.body.clientHeight - root.clientHeight;
|
||||
if (top > maxTop) {
|
||||
top = maxTop;
|
||||
}
|
||||
root.style.top = `${top}px`;
|
||||
config.value.pos = top;
|
||||
appStore().save();
|
||||
}
|
||||
onMounted(async () => {
|
||||
recoverAssistantTop();
|
||||
window.addEventListener("resize", () => {
|
||||
const root = toRaw(rootEl.value) as HTMLDivElement;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
updateAssistantTop(root.offsetTop);
|
||||
});
|
||||
return;
|
||||
});
|
||||
|
||||
const picking = ref(false);
|
||||
const rootEl = ref<HTMLDivElement>(null);
|
||||
const showBtns = ref(true);
|
||||
if (config.value.autoHide) {
|
||||
showBtns.value = false;
|
||||
}
|
||||
let autoHideTimer = null;
|
||||
let isDraging = false;
|
||||
return {
|
||||
showBtns,
|
||||
listArray,
|
||||
rootEl,
|
||||
picking,
|
||||
onMouseEnterCocosLogo() {
|
||||
clearTimeout(autoHideTimer);
|
||||
showBtns.value = true;
|
||||
},
|
||||
onCocosLogoClick() {
|
||||
showBtns.value = !showBtns.value;
|
||||
},
|
||||
onContextMenuRoot(event: MouseEvent) {
|
||||
const arr: IUiMenuItem[] = [
|
||||
{
|
||||
name: "auto hide",
|
||||
selected: config.value.autoHide,
|
||||
callback: (item) => {
|
||||
config.value.autoHide = !config.value.autoHide;
|
||||
appStore().save();
|
||||
ga(GA_EventName.MouseMenu, item.name);
|
||||
if (!config.value.autoHide) {
|
||||
clearTimeout(autoHideTimer);
|
||||
showBtns.value = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
ccui.menu.showMenuByMouseEvent(event, arr);
|
||||
},
|
||||
onMouseLeaveRoot(event: MouseEvent) {
|
||||
if (isDraging) {
|
||||
return;
|
||||
}
|
||||
if (!config.value.autoHide) {
|
||||
return;
|
||||
}
|
||||
autoHideTimer = setTimeout(() => {
|
||||
showBtns.value = false;
|
||||
}, 500);
|
||||
},
|
||||
onMouseDown(event: MouseEvent) {
|
||||
const root = toRaw(rootEl.value) as HTMLDivElement;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const startY = event.pageY;
|
||||
const startTop = root.offsetTop;
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
isDraging = true;
|
||||
const dy = e.pageY - startY;
|
||||
const top = startTop + dy;
|
||||
updateAssistantTop(top);
|
||||
}
|
||||
|
||||
function onMouseUp(e: MouseEvent) {
|
||||
isDraging = false;
|
||||
document.removeEventListener("mousemove", onMouseMove, true);
|
||||
document.removeEventListener("mouseup", onMouseUp, true);
|
||||
}
|
||||
document.addEventListener("mousemove", onMouseMove, true);
|
||||
document.addEventListener("mouseup", onMouseUp, true);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@x: 1px;
|
||||
@r: 8deg;
|
||||
@keyframes color-change {
|
||||
0% {
|
||||
color: #f00;
|
||||
transform: rotate(0) translateX(0px);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-@r) translateX(-@x);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(@r) translateX(@x);
|
||||
}
|
||||
50% {
|
||||
color: #0f0;
|
||||
}
|
||||
60% {
|
||||
transform: rotate(-@r) translateX(-@x);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(@r) translateX(@x);
|
||||
}
|
||||
100% {
|
||||
color: #f00;
|
||||
transform: rotate(0) translateX(0px);
|
||||
}
|
||||
}
|
||||
.ad {
|
||||
position: fixed;
|
||||
box-shadow: 0px 0px 6px 1px rgb(255, 255, 255);
|
||||
//z-index: 99999;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
// overflow: hidden;
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
background-color: rgb(0, 0, 0);
|
||||
border: 1px solid black;
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.btns {
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: white;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: rgb(101, 163, 249);
|
||||
}
|
||||
&:active {
|
||||
color: rgb(255, 187, 0);
|
||||
}
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
.ani_shop_cart {
|
||||
animation: color-change 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cocos {
|
||||
cursor: move;
|
||||
font-size: 20px;
|
||||
color: rgb(85, 192, 224);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
src/scripts/inject-view/banner.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div v-if="data" class="banner" :class="ani" @click="onClick" :title="data.tip" :style="getStyle()">
|
||||
<div class="text">
|
||||
<span v-if="data.name">
|
||||
{{ data.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, PropType, ref, toRaw } from "vue";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { emitter, Msg } from "./const";
|
||||
import { AdItem } from "./loader";
|
||||
import { ga } from "./util";
|
||||
export default defineComponent({
|
||||
name: "banner",
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<AdItem>,
|
||||
default: () => new AdItem(),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
function chageAd(v: AdItem) {
|
||||
console.log("show ad: ", JSON.stringify(v));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(Msg.ChangeAd, chageAd);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
emitter.off(Msg.ChangeAd, chageAd);
|
||||
});
|
||||
const ani = ref("");
|
||||
return {
|
||||
ani,
|
||||
getStyle() {
|
||||
const img = props.data.img;
|
||||
if (img) {
|
||||
return `background-image: url(${img})`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
const url = toRaw(props.data.store);
|
||||
if (url) {
|
||||
window.open(url);
|
||||
ga(GA_EventName.ClickPluginLink, url);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@keyframes flip-out {
|
||||
0% {
|
||||
transform: rotateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(90deg);
|
||||
}
|
||||
}
|
||||
@keyframes flip-in {
|
||||
0% {
|
||||
transform: rotateX(90deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(0);
|
||||
}
|
||||
}
|
||||
.banner-out {
|
||||
animation: flip-out 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955) both;
|
||||
}
|
||||
.banner-in {
|
||||
animation: flip-in 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955) both;
|
||||
}
|
||||
.banner {
|
||||
border: 2px solid #d2d2d2;
|
||||
background-color: #ffffff;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
min-height: 50px;
|
||||
max-height: 50px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: flex-end;
|
||||
|
||||
&:hover {
|
||||
border: 2px solid #949494;
|
||||
|
||||
background-color: #d1d1d1;
|
||||
}
|
||||
.text {
|
||||
user-select: none;
|
||||
flex: 1;
|
||||
padding-bottom: 2px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
span {
|
||||
color: #000000c4;
|
||||
background-color: #afafaf6b;
|
||||
padding: 1px 4px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/scripts/inject-view/const.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
export const Msg = {
|
||||
ChangeAd: "ChangeAd",
|
||||
};
|
||||
export const emitter = new TinyEmitter();
|
||||
157
src/scripts/inject-view/github.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
export interface MirrorInfo {
|
||||
/**
|
||||
* 请求的url
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 上次请求成功的时间
|
||||
*/
|
||||
time: number;
|
||||
}
|
||||
class Config {
|
||||
private key = "cc-inspector-ad-config";
|
||||
private data: MirrorInfo[] = [];
|
||||
constructor() {
|
||||
const cfg = localStorage.getItem(this.key);
|
||||
if (cfg) {
|
||||
try {
|
||||
const ret = JSON.parse(cfg) as MirrorInfo[];
|
||||
if (ret) {
|
||||
ret.forEach((el) => {
|
||||
this.data.push({ name: el.name, time: el.time });
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
save(name: string, time: number) {
|
||||
const ret = this.data.find((el) => el.name === name);
|
||||
if (ret) {
|
||||
ret.time = time;
|
||||
} else {
|
||||
this.data.push({ name: name, time: time } as MirrorInfo);
|
||||
}
|
||||
localStorage.setItem(this.key, JSON.stringify(this.data));
|
||||
}
|
||||
getTime(url: string) {
|
||||
const ret = this.data.find((el) => el.name === url);
|
||||
if (ret) {
|
||||
return ret.time;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
export class GithubMirror {
|
||||
owner: string = "tidys";
|
||||
repo: string = "cc-inspector-ad";
|
||||
branch: string = "main";
|
||||
/**
|
||||
* 上次请求成功的时间
|
||||
*/
|
||||
time: number = 0;
|
||||
/**
|
||||
* 镜像的名字
|
||||
*/
|
||||
name: string = "";
|
||||
private calcUrl: Function;
|
||||
constructor(name: string, cb) {
|
||||
this.name = name;
|
||||
this.time = cfg.getTime(name);
|
||||
this.calcUrl = cb;
|
||||
}
|
||||
public getUrl(file: string) {
|
||||
if (!file) {
|
||||
return "";
|
||||
}
|
||||
if (this.calcUrl) {
|
||||
return this.calcUrl(this.owner, this.repo, this.branch, file);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
public async getData(file: string) {
|
||||
const url = this.getUrl(file);
|
||||
if (url) {
|
||||
const data = await this.reqFecth(url);
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private reqFecth(url: string): Promise<Object | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`req ad: ${url}`);
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
resolve(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
const cfg = new Config();
|
||||
export class GithubMirrorMgr {
|
||||
mirrors: GithubMirror[] = [];
|
||||
constructor() {
|
||||
// 使用国内gitub镜像来达到下载远程配置文件的目的
|
||||
this.mirrors.push(
|
||||
new GithubMirror("github", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
this.mirrors.push(
|
||||
new GithubMirror("bgithub", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://raw.bgithub.xyz/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
this.mirrors.push(
|
||||
new GithubMirror("kkgithub", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://raw.kkgithub.com/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
|
||||
this.mirrors.push(
|
||||
new GithubMirror("xiaohei", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://raw-githubusercontent.xiaohei.me/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
|
||||
this.mirrors.push(
|
||||
new GithubMirror("gh-proxy", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://gh-proxy.com/raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
this.mirrors.push(
|
||||
new GithubMirror("ghproxy", (owner: string, repo: string, branch: string, file: string) => {
|
||||
return `https://ghproxy.net/https://raw.githubusercontent.com/${owner}/${repo}/refs/heads/${branch}/${file}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
async getData(file: string): Promise<Object | null> {
|
||||
this.mirrors.sort((a, b) => b.time - a.time);
|
||||
for (let i = 0; i < this.mirrors.length; i++) {
|
||||
const mirror = this.mirrors[i];
|
||||
const data = await mirror.getData(file);
|
||||
if (data) {
|
||||
const time = new Date().getTime();
|
||||
mirror.time = time;
|
||||
cfg.save(mirror.name, time);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getFileUrl(file: string): string {
|
||||
if (!file) {
|
||||
return "";
|
||||
}
|
||||
this.mirrors.sort((a, b) => b.time - a.time);
|
||||
const url = this.mirrors[0].getUrl(file);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
export const githubMirrorMgr = new GithubMirrorMgr();
|
||||
116
src/scripts/inject-view/loader.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import CCPlugin from "../../../cc-plugin.config";
|
||||
import { githubMirrorMgr } from "./github";
|
||||
export class AdItem {
|
||||
/**
|
||||
* 广告的名字
|
||||
*/
|
||||
name: string = "";
|
||||
/**
|
||||
* 鼠标悬浮提示
|
||||
*/
|
||||
tip: string = "";
|
||||
/**
|
||||
* 插件的试用地址
|
||||
*/
|
||||
try: string = "";
|
||||
/**
|
||||
* 广告的store购买链接
|
||||
*/
|
||||
store: string = "";
|
||||
/**
|
||||
* 广告的展示时间,单位s
|
||||
*/
|
||||
duration: number = 0;
|
||||
/**
|
||||
* 广告的有效性
|
||||
*/
|
||||
valid: boolean = true;
|
||||
/**
|
||||
* 背景图
|
||||
*/
|
||||
img: string = "";
|
||||
parse(data: AdItem) {
|
||||
this.name = data.name;
|
||||
this.store = data.store || "";
|
||||
this.parseStore();
|
||||
|
||||
this.try = data.try || "";
|
||||
this.tip = data.tip || "";
|
||||
this.duration = data.duration || 0;
|
||||
this.valid = !!data.valid;
|
||||
const img = data.img || "";
|
||||
this.img = githubMirrorMgr.getFileUrl(img);
|
||||
return this;
|
||||
}
|
||||
parseStore() {
|
||||
const flag = "${git}";
|
||||
if (this.store.startsWith(flag)) {
|
||||
const file = this.store.split(flag)[1];
|
||||
this.store = githubMirrorMgr.getFileUrl(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class AdData {
|
||||
desc: string = "";
|
||||
/**
|
||||
* 是否启用广告
|
||||
*/
|
||||
valid: boolean = false;
|
||||
/**
|
||||
* 多少分钟不再展示,单位分钟,默认10分钟
|
||||
*/
|
||||
showDuration: number = 10;
|
||||
/**
|
||||
* 底部广告多少秒滚动一次
|
||||
*/
|
||||
scrollDuration: number = 3;
|
||||
/**
|
||||
* 将位置随机打乱,保证用户每次看到的插件数量不一样,提高转换率
|
||||
*/
|
||||
randomIndex: boolean = false;
|
||||
/**
|
||||
* 展示的广告数量,-1为所有
|
||||
*/
|
||||
showCount: number = -1;
|
||||
|
||||
data: Array<AdItem> = [];
|
||||
parse(data: AdData) {
|
||||
this.desc = data.desc;
|
||||
this.valid = !!data.valid;
|
||||
this.showDuration = data.showDuration || 10;
|
||||
this.scrollDuration = data.scrollDuration || 3;
|
||||
this.randomIndex = !!data.randomIndex;
|
||||
this.showCount = data.showCount || -1;
|
||||
|
||||
if (data.data) {
|
||||
if (this.randomIndex) {
|
||||
data.data.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
data.data.forEach((el) => {
|
||||
if (this.showCount !== -1 && this.data.length >= this.showCount) {
|
||||
return;
|
||||
}
|
||||
const item = new AdItem().parse(el);
|
||||
if (!item.duration) {
|
||||
console.warn(`add failed, ad.duration is ${item.duration}, ${JSON.stringify(item)}`);
|
||||
return;
|
||||
}
|
||||
if (!item.valid) {
|
||||
console.warn(`add failed, ad is invalid, ${JSON.stringify(item)}`);
|
||||
return;
|
||||
}
|
||||
this.data.push(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdData(): Promise<AdData | null> {
|
||||
const data = await githubMirrorMgr.getData(`ad-${CCPlugin.manifest.version}.json`);
|
||||
if (data) {
|
||||
const ad = new AdData();
|
||||
ad.parse(data as AdData);
|
||||
return ad;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
58
src/scripts/inject-view/memory-draw.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export class Memory {
|
||||
public time: number = 0;
|
||||
public jsHeapSizeLimit: number = 0;
|
||||
public totalJSHeapSize: number = 0;
|
||||
public usedJSHeapSize: number = 0;
|
||||
constructor() {
|
||||
this.update();
|
||||
}
|
||||
update() {
|
||||
const memory = window.performance["memory"];
|
||||
if (memory) {
|
||||
this.time = Date.now();
|
||||
this.jsHeapSizeLimit = memory.jsHeapSizeLimit;
|
||||
this.totalJSHeapSize = memory.totalJSHeapSize;
|
||||
this.usedJSHeapSize = memory.usedJSHeapSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryDraw {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private width: number = 0;
|
||||
private height: number = 0;
|
||||
private memoryHistory: Memory[] = [];
|
||||
init(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d");
|
||||
this.width = canvas.width;
|
||||
this.height = canvas.height;
|
||||
this.clearBg();
|
||||
this.initTimer();
|
||||
}
|
||||
private initTimer() {
|
||||
setInterval(() => {
|
||||
const m = new Memory();
|
||||
this.memoryHistory.push(m);
|
||||
this.update();
|
||||
}, 300);
|
||||
}
|
||||
private update() {
|
||||
this.clearBg();
|
||||
for (let i = 0; i < this.memoryHistory.length; i++) {
|
||||
const m = this.memoryHistory[i];
|
||||
this.drawLine(i);
|
||||
}
|
||||
}
|
||||
private clearBg() {
|
||||
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||
this.ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
||||
}
|
||||
private lineWidth = 10;
|
||||
private drawLine(i: number) {
|
||||
this.ctx.fillStyle = "rgba(255, 37, 37, 0.5)";
|
||||
this.ctx.fillRect(0, 0, i * this.lineWidth, this.height / 2);
|
||||
}
|
||||
}
|
||||
62
src/scripts/inject-view/memory.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="memory">
|
||||
<canvas class="canvas" ref="elCanvas"></canvas>
|
||||
<div class="info">
|
||||
<div class="txt">{{ transformSize(memory.usedJSHeapSize) }}</div>
|
||||
<div class="txt">{{ transformSize(memory.totalJSHeapSize) }}</div>
|
||||
<div class="txt">{{ transformSize(memory.jsHeapSizeLimit) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, onMounted, ref, toRaw } from "vue";
|
||||
import { Memory, MemoryDraw } from "./memory-draw";
|
||||
import { transformSize } from "./util";
|
||||
const { CCButton } = ccui.components;
|
||||
|
||||
export default defineComponent({
|
||||
name: "memory",
|
||||
components: { CCButton },
|
||||
setup(props, { emit }) {
|
||||
const memoryDraw = new MemoryDraw();
|
||||
const memory = ref<Memory>(new Memory());
|
||||
setInterval(() => {
|
||||
memory.value.update();
|
||||
}, 300);
|
||||
|
||||
onMounted(() => {
|
||||
const el = toRaw(elCanvas.value);
|
||||
if (el) {
|
||||
// memoryDraw.init(el as HTMLCanvasElement);
|
||||
}
|
||||
});
|
||||
const elCanvas = ref<HTMLCanvasElement>(null);
|
||||
return { memory, transformSize, elCanvas };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.memory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.canvas {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.txt {
|
||||
user-select: none;
|
||||
margin: 0 3px;
|
||||
}
|
||||
:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
src/scripts/inject-view/res/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1737262301480" class="icon" viewBox="0 0 1028 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2139" width="16.0625" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M597.54570376 512l219.97466684-219.97466683c24.44162965-24.44162965 24.44162965-61.10407412 0-85.54570377-24.44162965-24.44162965-61.10407412-24.44162965-85.54570377 0L512 426.45429624 292.02533317 206.4796294C267.58370352 182.03799974 230.92125905 182.03799974 206.4796294 206.4796294c-24.44162965 24.44162965-24.44162965 61.10407412 0 85.54570377L426.45429624 512l-219.97466684 219.97466683c-24.44162965 24.44162965-24.44162965 61.10407412 0 85.54570377 12.22081482 12.22081482 28.51523459 16.29441978 44.80965436 16.29441976s32.58883953-4.07360495 44.80965435-16.29441976L512 597.54570376l219.97466683 219.97466684c12.22081482 12.22081482 28.51523459 16.29441978 44.80965436 16.29441977s32.58883953-4.07360495 44.80965436-16.29441977c24.44162965-24.44162965 24.44162965-61.10407412 0-85.54570377L597.54570376 512z" fill="#cdcdcd" p-id="2140"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
src/scripts/inject-view/res/left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1737262395969" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2346" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M704 868c-8.3 0-16.5-2.8-23.1-8.3l-384-320c-8.2-6.8-13-17-13-27.7s4.7-20.8 13-27.7l384-320c10.7-8.9 25.7-10.9 38.3-4.9C731.9 165.3 740 178 740 192v640c0 14-8.1 26.7-20.7 32.6-4.9 2.3-10.1 3.4-15.3 3.4z" fill="#dbdbdb" p-id="2347"></path></svg>
|
||||
|
After Width: | Height: | Size: 574 B |
1
src/scripts/inject-view/res/right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1737262412557" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2507" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M304.7 864.6C292.1 858.7 284 846 284 832V192c0-14 8.1-26.7 20.7-32.6 12.6-5.9 27.6-4 38.3 4.9l384 320c8.2 6.8 13 17 13 27.7s-4.7 20.8-13 27.7l-384 320c-6.6 5.5-14.8 8.3-23.1 8.3-5.1 0-10.3-1.1-15.2-3.4z" fill="#dbdbdb" p-id="2508"></path></svg>
|
||||
|
After Width: | Height: | Size: 575 B |
36
src/scripts/inject-view/store.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ref, toRaw } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import profile from "cc-plugin/src/ccp/profile";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
export class ConfigData {
|
||||
/**
|
||||
* 用户拖动的位置
|
||||
*/
|
||||
pos: number = 0;
|
||||
/**
|
||||
* 是否自动隐藏
|
||||
*/
|
||||
autoHide: boolean = true;
|
||||
/**
|
||||
* 是否只拾取顶部元素
|
||||
*/
|
||||
pickTop: boolean = true;
|
||||
}
|
||||
|
||||
export const appStore = defineStore("app", () => {
|
||||
const config = ref<ConfigData>(new ConfigData());
|
||||
return {
|
||||
config,
|
||||
init() {
|
||||
profile.init(new ConfigData(), pluginConfig);
|
||||
const data = profile.load(`${pluginConfig.manifest.name}-assistant.json`) as ConfigData;
|
||||
config.value.autoHide = data.autoHide;
|
||||
config.value.pos = data.pos;
|
||||
config.value.pickTop = data.pickTop;
|
||||
},
|
||||
save() {
|
||||
const cfg = toRaw(config.value);
|
||||
profile.save(cfg);
|
||||
},
|
||||
};
|
||||
});
|
||||
29
src/scripts/inject-view/util.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { DocumentEvent, GoogleAnalyticsData } from "../const";
|
||||
|
||||
export function ga(event: GA_EventName, params: string = "") {
|
||||
const detail = { event, params } as GoogleAnalyticsData;
|
||||
const e = new CustomEvent(DocumentEvent.GoogleAnalytics, { detail });
|
||||
document.dispatchEvent(e);
|
||||
}
|
||||
|
||||
export function transformSize(size: number) {
|
||||
if (!size) return "0B";
|
||||
size = parseInt(size.toString());
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
}
|
||||
size = size / 1024;
|
||||
if (size < 1024) {
|
||||
return size.toFixed(2) + "KB";
|
||||
}
|
||||
size = size / 1024;
|
||||
if (size < 1024) {
|
||||
return size.toFixed(2) + "MB";
|
||||
}
|
||||
size = size / 1024;
|
||||
if (size < 1024) {
|
||||
return size.toFixed(2) + "GB";
|
||||
}
|
||||
return size;
|
||||
}
|
||||
19
src/scripts/inject-view/web-test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 这个是web测试inject_view的入口,实际chrome插件不应该走这个界面
|
||||
*/
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp } from "vue";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
import App from "./app.vue";
|
||||
export default CCP.init(pluginConfig, {
|
||||
ready: function (rootElement: any, args: any) {
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
app.use(ccui);
|
||||
app.mount(rootElement);
|
||||
},
|
||||
});
|
||||
239
src/scripts/inject/enumConfig.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
declare const cc: any;
|
||||
|
||||
export function getEnumListConfig() {
|
||||
const enumConfig: Array<{
|
||||
type: any;
|
||||
list: Array<{
|
||||
key: string;
|
||||
values: () => Array<{ name: string; value: number }>;
|
||||
}>;
|
||||
}> = [
|
||||
{
|
||||
type: cc.Widget,
|
||||
list: [
|
||||
{
|
||||
key: "alignMode",
|
||||
values: () => {
|
||||
return cc.Widget.AlignMode.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Button,
|
||||
list: [
|
||||
{
|
||||
key: "transition",
|
||||
values: () => {
|
||||
return cc.Button.Transition.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Sprite,
|
||||
list: [
|
||||
{
|
||||
key: "sizeMode",
|
||||
values: () => {
|
||||
return cc.Sprite.SizeMode.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
values: () => {
|
||||
return cc.Sprite.Type.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Mask,
|
||||
list: [
|
||||
{
|
||||
key: "type",
|
||||
values() {
|
||||
return cc.Mask.Type.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Label,
|
||||
list: [
|
||||
{
|
||||
key: "cacheMode",
|
||||
values() {
|
||||
return cc.Label.CacheMode.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "overflow",
|
||||
values() {
|
||||
return cc.Label.Overflow.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "verticalAlign",
|
||||
values() {
|
||||
return cc.Label.VerticalAlign.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "horizontalAlign",
|
||||
values() {
|
||||
return cc.Label.HorizontalAlign.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Slider,
|
||||
list: [
|
||||
{
|
||||
key: "direction",
|
||||
values() {
|
||||
return cc.Slider.Direction.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.PageView,
|
||||
list: [
|
||||
{
|
||||
key: "direction",
|
||||
values() {
|
||||
return cc.PageView.Direction.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "sizeMode",
|
||||
values() {
|
||||
return cc.PageView.SizeMode.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.PageViewIndicator,
|
||||
list: [
|
||||
{
|
||||
key: "direction",
|
||||
values() {
|
||||
return cc.PageViewIndicator.Direction.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.RichText,
|
||||
list: [
|
||||
{
|
||||
key: "cacheMode",
|
||||
values() {
|
||||
return cc.Label.CacheMode.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "horizontalAlign",
|
||||
values() {
|
||||
return cc.RichText.HorizontalAlign.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "verticalAlign",
|
||||
values() {
|
||||
return cc.RichText.VerticalAlign.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.ProgressBar,
|
||||
list: [
|
||||
{
|
||||
key: "mode",
|
||||
values() {
|
||||
return cc.ProgressBar.Mode.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Scrollbar,
|
||||
list: [
|
||||
{
|
||||
key: "direction",
|
||||
values() {
|
||||
return cc.Scrollbar.Direction.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.EditBox,
|
||||
list: [
|
||||
{
|
||||
key: "inputMode",
|
||||
values() {
|
||||
return cc.EditBox.InputMode.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "inputFlag",
|
||||
values() {
|
||||
return cc.EditBox.InputFlag.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.Layout,
|
||||
list: [
|
||||
{
|
||||
key: "resizeMode",
|
||||
values() {
|
||||
return cc.Layout.ResizeMode.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
values() {
|
||||
return cc.Layout.Type.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "startAxis",
|
||||
values() {
|
||||
return cc.Layout.AxisDirection.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "horizontalDirection",
|
||||
values() {
|
||||
return cc.Layout.HorizontalDirection.__enums__;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "verticalDirection",
|
||||
values() {
|
||||
return cc.Layout.VerticalDirection.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: cc.VideoPlayer,
|
||||
list: [
|
||||
{
|
||||
key: "resourceType",
|
||||
values() {
|
||||
return cc.VideoPlayer.ResourceType.__enums__;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return enumConfig;
|
||||
}
|
||||
32
src/scripts/inject/event.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { debugLog, Msg, Page, PluginEvent } from "../../core/types";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { DocumentEvent, GoogleAnalyticsData } from "../const";
|
||||
import { Terminal } from "../terminal";
|
||||
|
||||
export class InjectEvent {
|
||||
protected terminal = new Terminal("Inject ");
|
||||
constructor() {
|
||||
document.addEventListener(DocumentEvent.Content2Inject, (event: CustomEvent) => {
|
||||
const pluginEvent: PluginEvent = PluginEvent.create(event.detail);
|
||||
debugLog && console.log(...this.terminal.chunkMessage(pluginEvent.toChunk()));
|
||||
this.onMessage(pluginEvent);
|
||||
});
|
||||
}
|
||||
onMessage(data: PluginEvent) {}
|
||||
sendMsgToContent(msg: Msg, data: any) {
|
||||
const detail = new PluginEvent(Page.Inject, Page.Content, msg, data);
|
||||
debugLog && console.log(...this.terminal.chunkSend(detail.toChunk()));
|
||||
const event = new CustomEvent(DocumentEvent.Inject2Content, { detail });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
sendEngineVersion(version: string) {
|
||||
const detail = version;
|
||||
const event = new CustomEvent(DocumentEvent.EngineVersion, { detail });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
sendAppVersion(version: string) {
|
||||
const detail = { event: GA_EventName.AppVersion, params: version } as GoogleAnalyticsData;
|
||||
const event = new CustomEvent(DocumentEvent.GoogleAnalytics, { detail });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
97
src/scripts/inject/hint/adapter.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
declare const cc: any;
|
||||
export interface DrawOptions {
|
||||
fill: boolean;
|
||||
fillColor: string;
|
||||
stroke: boolean;
|
||||
strokeColor: string;
|
||||
}
|
||||
export class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
export class RectPoints {
|
||||
points: Point[] = [];
|
||||
get len() {
|
||||
return this.points.length;
|
||||
}
|
||||
add(point: Point) {
|
||||
this.points.push(point);
|
||||
}
|
||||
at(index: number) {
|
||||
return this.points[index];
|
||||
}
|
||||
test(width: number, height: number) {
|
||||
this.points.push(new Point(0, 0));
|
||||
this.points.push(new Point(width, 0));
|
||||
this.points.push(new Point(width, height));
|
||||
this.points.push(new Point(0, height));
|
||||
}
|
||||
}
|
||||
export class HintAdapter {
|
||||
protected draw = null;
|
||||
constructor() {}
|
||||
resetIndex() {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
getRectPoints(node: any): RectPoints | null {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
clear() {
|
||||
if (this.draw) {
|
||||
this.draw.clear();
|
||||
}
|
||||
}
|
||||
convertMousePos(event: MouseEvent, canvas: HTMLCanvasElement): { x: number; y: number } {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
hitTest(node: any, x: number, y: number): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
initDrawNode() {
|
||||
if (this.draw && !this.draw.isValid) {
|
||||
this.draw = null;
|
||||
}
|
||||
if (this.draw) {
|
||||
return;
|
||||
}
|
||||
const scene = cc.director.getScene();
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
let node = new cc.Node("draw-node");
|
||||
this.addDraw(scene, node);
|
||||
this.draw = node.addComponent(cc.Graphics || cc.GraphicsComponent);
|
||||
}
|
||||
public isDrawValid() {
|
||||
return this.draw && this.draw.isValid;
|
||||
}
|
||||
protected addDraw(scene: any, node: any) {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
public drawRect(points: RectPoints, opts: DrawOptions) {
|
||||
this.draw.lineWidth = 2;
|
||||
for (let i = 0; i < points.len; i++) {
|
||||
const p = points.at(i);
|
||||
if (i === 0) {
|
||||
this.draw.moveTo(p.x, p.y);
|
||||
} else {
|
||||
this.draw.lineTo(p.x, p.y);
|
||||
}
|
||||
}
|
||||
if (points.len) {
|
||||
this.draw.close();
|
||||
}
|
||||
if (opts.stroke) {
|
||||
this.draw.strokeColor = new cc.Color().fromHEX(opts.strokeColor);
|
||||
this.draw.stroke();
|
||||
}
|
||||
if (opts.fill) {
|
||||
this.draw.fillColor = new cc.Color().fromHEX(opts.fillColor);
|
||||
this.draw.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/scripts/inject/hint/hint-v2.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { HintAdapter, Point, RectPoints } from "./adapter";
|
||||
declare const cc: any;
|
||||
|
||||
export class HintV2 extends HintAdapter {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
protected addDraw(scene: any, node: any): void {
|
||||
scene.addChild(node);
|
||||
}
|
||||
private canvasBoundingRect = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
adjustedTop: 0,
|
||||
adjustedLeft: 0,
|
||||
};
|
||||
private _updateCanvasBoundingRect() {
|
||||
// @ts-ignore
|
||||
const element: any = cc.game.canvas;
|
||||
var docElem = document.documentElement;
|
||||
var leftOffset = window.pageXOffset - docElem.clientLeft;
|
||||
var topOffset = window.pageYOffset - docElem.clientTop;
|
||||
if (element.getBoundingClientRect) {
|
||||
var box = element.getBoundingClientRect();
|
||||
this.canvasBoundingRect.left = box.left + leftOffset;
|
||||
this.canvasBoundingRect.top = box.top + topOffset;
|
||||
this.canvasBoundingRect.width = box.width;
|
||||
this.canvasBoundingRect.height = box.height;
|
||||
} else if (element instanceof HTMLCanvasElement) {
|
||||
this.canvasBoundingRect.left = leftOffset;
|
||||
this.canvasBoundingRect.top = topOffset;
|
||||
this.canvasBoundingRect.width = element.width;
|
||||
this.canvasBoundingRect.height = element.height;
|
||||
} else {
|
||||
this.canvasBoundingRect.left = leftOffset;
|
||||
this.canvasBoundingRect.top = topOffset;
|
||||
this.canvasBoundingRect.width = parseInt(element.style.width);
|
||||
this.canvasBoundingRect.height = parseInt(element.style.height);
|
||||
}
|
||||
}
|
||||
getPointByEvent(event: MouseEvent) {
|
||||
this._updateCanvasBoundingRect();
|
||||
if (event.pageX != null)
|
||||
//not avalable in <= IE8
|
||||
return { x: event.pageX, y: event.pageY };
|
||||
|
||||
this.canvasBoundingRect.left -= document.body.scrollLeft;
|
||||
this.canvasBoundingRect.top -= document.body.scrollTop;
|
||||
|
||||
return { x: event.clientX, y: event.clientY };
|
||||
}
|
||||
convertMousePos(event: MouseEvent, canvas: HTMLCanvasElement): { x: number; y: number } {
|
||||
let location = this.getPointByEvent(event);
|
||||
// let p = cc.view._convertMouseToLocationInView(location, this.canvasBoundingRect);
|
||||
let p = cc.view.convertToLocationInView(location.x, location.y, this.canvasBoundingRect);
|
||||
let scaleX = cc.view._scaleX;
|
||||
let scaleY = cc.view._scaleY;
|
||||
let viewport = cc.view._viewportRect;
|
||||
let x = (p.x - viewport.x) / scaleX;
|
||||
let y = (p.y - viewport.y) / scaleY;
|
||||
|
||||
// let position = cc.v2(event.offsetX, event.offsetY);
|
||||
// let size = cc.view.getDesignResolutionSize();
|
||||
// let rect = { left: 0, top: 0, width: size.width, height: size.height };
|
||||
// cc.view._convertMouseToLocationInView(position, rect);
|
||||
// let wordPos = cc.v2();
|
||||
// cc.Camera.main.getScreenToWorldPoint(position, wordPos);
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
hitTest(node: any, x: number, y: number): boolean {
|
||||
// let rect = item.getBoundingBox();
|
||||
// let p = item.parent.convertToNodeSpaceAR(wordPos);
|
||||
// if (rect.contains(p)) {
|
||||
// return item;
|
||||
// }
|
||||
const mask = this._searchComponentsInParent(node, cc.Mask);
|
||||
const b = node._hitTest(cc.v2(x, y), { mask });
|
||||
return b;
|
||||
}
|
||||
private _searchComponentsInParent(node: any, comp: any) {
|
||||
if (comp) {
|
||||
let index = 0;
|
||||
let list = null;
|
||||
for (var curr = node; curr && cc.Node.isNode(curr); curr = curr._parent, ++index) {
|
||||
if (curr.getComponent(comp)) {
|
||||
let next = {
|
||||
index: index,
|
||||
node: curr,
|
||||
};
|
||||
|
||||
if (list) {
|
||||
list.push(next);
|
||||
} else {
|
||||
list = [next];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
resetIndex(): void {
|
||||
const node = this.draw.node;
|
||||
node.zIndex = node.parent.children.length;
|
||||
}
|
||||
getRectPoints(node: any): RectPoints | null {
|
||||
const points: RectPoints = new RectPoints();
|
||||
const { anchorX, anchorY, width, height } = node;
|
||||
const x = -anchorX * width;
|
||||
const y = -anchorY * height;
|
||||
const matrixm = node._worldMatrix.m;
|
||||
const m00 = matrixm[0],
|
||||
m01 = matrixm[1],
|
||||
m04 = matrixm[4],
|
||||
m05 = matrixm[5],
|
||||
m12_tx = matrixm[12],
|
||||
m13_ty = matrixm[13];
|
||||
[
|
||||
new Point(x, y), //
|
||||
new Point(x + width, y),
|
||||
new Point(x + width, y + height),
|
||||
new Point(x, y + height),
|
||||
].forEach(({ x, y }: Point) => {
|
||||
const worldX = x * m00 + y * m04 + m12_tx; // x
|
||||
const worldY = x * m01 + y * m05 + m13_ty; // y
|
||||
const pos = this.draw.node.convertToNodeSpaceAR(cc.v2(worldX, worldY));
|
||||
points.add(new Point(pos.x, pos.y));
|
||||
});
|
||||
return points;
|
||||
}
|
||||
}
|
||||
149
src/scripts/inject/hint/hint-v3.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { HintAdapter, Point, RectPoints } from "./adapter";
|
||||
declare const cc: any;
|
||||
|
||||
export class HintV3 extends HintAdapter {
|
||||
resetIndex(): void {
|
||||
const node = this.draw.node;
|
||||
if (node.parent) {
|
||||
const len = node.parent.children.length;
|
||||
node.setSiblingIndex(len);
|
||||
}
|
||||
}
|
||||
private get transformComponent() {
|
||||
return cc.UITransformComponent || cc.UITransform;
|
||||
}
|
||||
hitTest(node: any, x: number, y: number): boolean {
|
||||
let hitTest = null;
|
||||
// hitTest = node._uiProps?.uiTransformComp?.hitTest;
|
||||
const tr = node.getComponent(this.transformComponent);
|
||||
if (tr) {
|
||||
if (tr.hitTest) {
|
||||
hitTest = tr.hitTest.bind(tr);
|
||||
} else if (tr.isHit) {
|
||||
// TODO: 3.3.1使用的是这个接口,hitTest有问题,有人反馈再说修复,老版本暂不花费太多精力
|
||||
hitTest = tr.isHit.bind(tr);
|
||||
}
|
||||
}
|
||||
if (hitTest) {
|
||||
let b = hitTest({ x, y }, 0);
|
||||
return b;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
convertMousePos(event: MouseEvent, canvas: HTMLCanvasElement): { x: number; y: number } {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
let x = event.clientX - rect.x;
|
||||
let y = rect.y + rect.height - event.clientY;
|
||||
x *= window.devicePixelRatio;
|
||||
y *= window.devicePixelRatio;
|
||||
return { x, y };
|
||||
}
|
||||
/**
|
||||
* 这种方式能够获取到优先绘制的canvas,也能保证线框在顶部,但是方案不完美,会收到node.layer的影响
|
||||
*/
|
||||
private getCanvas(scene: any) {
|
||||
const canvasArray: Array<{ canvas: any; index: number }> = [];
|
||||
scene.walk((item: any) => {
|
||||
if (cc.Canvas) {
|
||||
const comp = item.getComponent(cc.Canvas);
|
||||
if (comp) {
|
||||
const idx = comp.cameraComponent?.priority || 0;
|
||||
canvasArray.push({ canvas: comp, index: idx });
|
||||
}
|
||||
}
|
||||
});
|
||||
canvasArray.sort((a, b) => a.index - b.index);
|
||||
return canvasArray[0].canvas.node;
|
||||
}
|
||||
protected addDraw(scene: any, node: any): void {
|
||||
const canvas = this.getCanvas(scene);
|
||||
if (canvas) {
|
||||
if (canvas.layer) {
|
||||
node.layer = canvas.layer;
|
||||
}
|
||||
const tr = node.getComponent(this.transformComponent) || node.addComponent(this.transformComponent);
|
||||
if (tr) {
|
||||
tr.setContentSize(0, 0);
|
||||
}
|
||||
// FIXME: 多canvas的情况下,如果hover和select的节点不在一个canvas下,绘制线框有问题,暂时先不支持多canvas的情况
|
||||
canvas.addChild(node);
|
||||
}
|
||||
}
|
||||
getRectPoints(node: any): RectPoints | null {
|
||||
if (!node.worldPosition) {
|
||||
return null;
|
||||
}
|
||||
if (!this.transformComponent) {
|
||||
return null;
|
||||
}
|
||||
const tr = node.getComponent(this.transformComponent);
|
||||
if (!tr) {
|
||||
return null;
|
||||
}
|
||||
const { anchorPoint, width, height } = tr;
|
||||
const points: RectPoints = new RectPoints();
|
||||
const x = -anchorPoint.x * width;
|
||||
const y = -anchorPoint.y * height;
|
||||
const m = node.worldMatrix;
|
||||
|
||||
[
|
||||
new Point(x, y), //
|
||||
new Point(x + width, y),
|
||||
new Point(x + width, y + height),
|
||||
new Point(x, y + height),
|
||||
].forEach(({ x, y }: Point) => {
|
||||
let rhw = m.m03 * x + m.m07 * y + m.m15;
|
||||
rhw = rhw ? 1 / rhw : 1;
|
||||
let worldX = (m.m00 * x + m.m04 * y + m.m12) * rhw;
|
||||
let worldY = (m.m01 * x + m.m05 * y + m.m13) * rhw;
|
||||
let worldZ = (m.m02 * x + m.m06 * y + m.m14) * rhw;
|
||||
const pos = this.draw.getComponent(this.transformComponent).convertToNodeSpaceAR(cc.v3(worldX, worldY, worldZ));
|
||||
points.add(new Point(pos.x, pos.y));
|
||||
});
|
||||
|
||||
return points;
|
||||
}
|
||||
/**
|
||||
* 尝试实现wireframe模式
|
||||
*/
|
||||
private getTrangles(node: any) {
|
||||
const comps = node._components;
|
||||
if (comps && Array.isArray(comps)) {
|
||||
const m = node.worldMatrix;
|
||||
comps.forEach((comp) => {
|
||||
const { renderData } = comp;
|
||||
if (!renderData) {
|
||||
return;
|
||||
}
|
||||
const { data, _vc: VertexCount, _ic: IndexCount, chunk } = renderData;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取indexBuffer的索引顺序
|
||||
const ib = chunk.meshBuffer.iData;
|
||||
let indexOffset = chunk.meshBuffer.indexOffset;
|
||||
const vidOrigin = chunk.vertexOffset;
|
||||
const arr = [];
|
||||
for (let i = 0; i < IndexCount; i++) {
|
||||
const index = ib[indexOffset + i] - vidOrigin;
|
||||
arr.push(index);
|
||||
}
|
||||
|
||||
for (let j = 0; j < data.length; j++) {
|
||||
const item = data[j];
|
||||
const { x, y, z, u, v, color } = item;
|
||||
let rhw = m.m03 * x + m.m07 * y + m.m15;
|
||||
rhw = rhw ? 1 / rhw : 1;
|
||||
|
||||
let worldX = (m.m00 * x + m.m04 * y + m.m12) * rhw;
|
||||
let worldY = (m.m01 * x + m.m05 * y + m.m13) * rhw;
|
||||
let worldZ = (m.m02 * x + m.m06 * y + m.m14) * rhw;
|
||||
// const pos = this.draw.getComponent(transform).convertToNodeSpaceAR(cc.v3(worldX, worldY, worldZ));
|
||||
// points.points.push(new Point(pos.x, pos.y));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
217
src/scripts/inject/hint/index.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { IUiMenuItem } from "@xuyanfeng/cc-ui/types/cc-menu/const";
|
||||
import { throttle } from "lodash";
|
||||
import { toRaw } from "vue";
|
||||
import { Msg } from "../../../core/types";
|
||||
import { DocumentEvent } from "../../const";
|
||||
import { appStore } from "../../inject-view/store";
|
||||
import { Inspector } from "../inspector";
|
||||
import { DrawOptions, HintAdapter, RectPoints } from "./adapter";
|
||||
import { HintV2 } from "./hint-v2";
|
||||
import { HintV3 } from "./hint-v3";
|
||||
declare const cc: any;
|
||||
|
||||
/**
|
||||
* 只负责管理hint的流程
|
||||
*/
|
||||
export class Hint {
|
||||
private engineVersion: string = "";
|
||||
private hintAdapter: HintAdapter = null;
|
||||
public setEngineVersion(version: string) {
|
||||
this.engineVersion = version;
|
||||
if (version.startsWith("2.")) {
|
||||
this.hintAdapter = new HintV2();
|
||||
} else if (version.startsWith("3.")) {
|
||||
this.hintAdapter = new HintV3();
|
||||
}
|
||||
}
|
||||
private inspector: Inspector = null;
|
||||
constructor(inspector: Inspector) {
|
||||
this.inspector = inspector;
|
||||
document.addEventListener(DocumentEvent.InspectorClear, () => {
|
||||
this.cleanHover();
|
||||
this.cleanSelected();
|
||||
});
|
||||
document.addEventListener(DocumentEvent.GameInspectorBegan, (event: CustomEvent) => {
|
||||
const el = this.getTargetElement();
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = el.style.cursor;
|
||||
el.style.cursor = "zoom-in";
|
||||
|
||||
const mousedown = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
el.removeEventListener("mousedown", mousedown, true);
|
||||
el.removeEventListener("mousemove", mousemove, true);
|
||||
el.style.cursor = cursor;
|
||||
const e = new CustomEvent(DocumentEvent.GameInspectorEnd);
|
||||
document.dispatchEvent(e);
|
||||
|
||||
if (event.button === 0) {
|
||||
// 左键拾取
|
||||
this.updateHintDown(event, el);
|
||||
} else {
|
||||
this.updateHitMoveThrottle.cancel();
|
||||
// 其他按键取消
|
||||
this.cleanHover();
|
||||
}
|
||||
};
|
||||
const mousemove = (event: MouseEvent) => {
|
||||
this.updateHitMoveThrottle(event, el);
|
||||
};
|
||||
|
||||
el.addEventListener("mousemove", mousemove, true);
|
||||
el.addEventListener("mousedown", mousedown, true);
|
||||
});
|
||||
}
|
||||
private updateHitMoveThrottle = throttle(this.updateHintMove, 300);
|
||||
private updateHintMove(event: MouseEvent, canvas: HTMLCanvasElement) {
|
||||
const nodes = this.getMouseNodes(event, canvas);
|
||||
if (nodes.length) {
|
||||
const node = nodes[0];
|
||||
this.setHover(node);
|
||||
}
|
||||
}
|
||||
private getMouseNodes(event: MouseEvent, canvas: HTMLCanvasElement): Array<any> {
|
||||
this.inspector.updateTreeInfo(false);
|
||||
const { x, y } = this.hintAdapter.convertMousePos(event, canvas);
|
||||
const nodes = [];
|
||||
this.inspector.forEachNode((node) => {
|
||||
const b = this.hintAdapter.hitTest(node, x, y);
|
||||
if (b && node.active && node.activeInHierarchy) {
|
||||
nodes.push(node);
|
||||
}
|
||||
});
|
||||
nodes.reverse();
|
||||
return nodes;
|
||||
}
|
||||
private updateHintDown(event: MouseEvent, canvas: HTMLCanvasElement) {
|
||||
this.cleanHover();
|
||||
this.cleanSelected();
|
||||
const nodes = this.getMouseNodes(event, canvas);
|
||||
const pickTop = toRaw(appStore().config.pickTop);
|
||||
if (nodes.length === 1 || (pickTop && nodes.length)) {
|
||||
const item = nodes[0];
|
||||
this.cleanHover();
|
||||
this.setSelected(item);
|
||||
this.sendInspectNodeMsg(item);
|
||||
} else {
|
||||
const menu = nodes.map((item) => {
|
||||
const path = this.getPath(item);
|
||||
return {
|
||||
name: path,
|
||||
callback: () => {
|
||||
this.cleanHover();
|
||||
this.setSelected(item);
|
||||
this.sendInspectNodeMsg(item);
|
||||
},
|
||||
enter: () => {
|
||||
this.setHover(item);
|
||||
},
|
||||
leave: () => {
|
||||
this.cleanHover();
|
||||
},
|
||||
} as IUiMenuItem;
|
||||
});
|
||||
ccui.menu.showMenuByMouseEvent(event, menu, 0.8);
|
||||
}
|
||||
}
|
||||
private sendInspectNodeMsg(node: any) {
|
||||
if (node.uuid) {
|
||||
this.inspector.sendMsgToContent(Msg.InspectNode, node.uuid);
|
||||
}
|
||||
}
|
||||
private getPath(node: any) {
|
||||
let path = [];
|
||||
let parent = node;
|
||||
while (parent) {
|
||||
path.push(parent.name);
|
||||
parent = parent.parent;
|
||||
}
|
||||
path = path.reverse();
|
||||
return path.join("/");
|
||||
}
|
||||
private getTargetElement(): HTMLCanvasElement | null {
|
||||
// @ts-ignore
|
||||
if (typeof cc !== "undefined" && cc.game && cc.game.canvas) {
|
||||
// @ts-ignore
|
||||
return cc.game.canvas;
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
}
|
||||
public cleanHover() {
|
||||
this.hoverNodes = [];
|
||||
this.hintAdapter && this.hintAdapter.clear();
|
||||
}
|
||||
public cleanSelected() {
|
||||
this.selectedNodes = [];
|
||||
this.hintAdapter && this.hintAdapter.clear();
|
||||
}
|
||||
private hoverNodes = [];
|
||||
private selectedNodes = [];
|
||||
|
||||
public update() {
|
||||
if (!this.hintAdapter) {
|
||||
return;
|
||||
}
|
||||
this.hintAdapter.initDrawNode();
|
||||
if (!this.hintAdapter.isDrawValid()) {
|
||||
return;
|
||||
}
|
||||
this.hintAdapter.clear();
|
||||
this.hintAdapter.resetIndex();
|
||||
// this.testRect();
|
||||
this.hoverNodes.forEach((node) => {
|
||||
if (node.isValid) {
|
||||
this.hintNode(node, {
|
||||
fill: true,
|
||||
fillColor: "#00ff0055",
|
||||
stroke: true,
|
||||
strokeColor: "#ffffff44",
|
||||
});
|
||||
}
|
||||
});
|
||||
this.selectedNodes.forEach((node) => {
|
||||
if (node.isValid) {
|
||||
this.hintNode(node, {
|
||||
fill: false,
|
||||
fillColor: "#ff0000",
|
||||
stroke: true,
|
||||
strokeColor: "#ff0000",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
private testRect() {
|
||||
const points = new RectPoints();
|
||||
points.test(100, 100);
|
||||
this.hintAdapter.drawRect(points, {
|
||||
fill: true,
|
||||
fillColor: "#00ff0099",
|
||||
stroke: true,
|
||||
strokeColor: "#ff000099",
|
||||
});
|
||||
}
|
||||
public setHover(node: any) {
|
||||
if (node.isValid) {
|
||||
this.hoverNodes = [node];
|
||||
}
|
||||
}
|
||||
public setSelected(node: any) {
|
||||
if (node.isValid) {
|
||||
this.selectedNodes = [node];
|
||||
}
|
||||
}
|
||||
|
||||
private hintNode(node: any, opts: DrawOptions) {
|
||||
const points = this.hintAdapter.getRectPoints(node);
|
||||
if (!points) {
|
||||
return;
|
||||
}
|
||||
this.hintAdapter.drawRect(points, opts);
|
||||
}
|
||||
}
|
||||
4
src/scripts/inject/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Inspector } from "./inspector";
|
||||
const inspector = new Inspector();
|
||||
inspector.init();
|
||||
window["CCInspector"] = inspector;
|
||||
56
src/scripts/inject/inject-view.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp } from "vue";
|
||||
import { DocumentEvent } from "../const";
|
||||
import App from "../inject-view/app.vue";
|
||||
export class InjectView {
|
||||
private _inited = false;
|
||||
private css: string[] = [];
|
||||
private hasLoadCss = false;
|
||||
constructor() {
|
||||
document.addEventListener(DocumentEvent.LoadInjectCss, (event: CustomEvent) => {
|
||||
const cssArray: string[] = event.detail;
|
||||
this.css = cssArray;
|
||||
this.loadCss();
|
||||
});
|
||||
}
|
||||
public init() {
|
||||
if (this._inited) {
|
||||
return;
|
||||
}
|
||||
this._inited = true;
|
||||
this.createUI();
|
||||
}
|
||||
private loadCss() {
|
||||
if (this.hasLoadCss) {
|
||||
return;
|
||||
}
|
||||
if (this.css.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.hasLoadCss = true;
|
||||
this.css.forEach((css) => {
|
||||
const link = document.createElement("link");
|
||||
link.href = css;
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
private createUI() {
|
||||
const el = document.createElement("div");
|
||||
el.setAttribute("app", "");
|
||||
el.style.zIndex = "9000";
|
||||
document.body.appendChild(el);
|
||||
// load css
|
||||
this.loadCss();
|
||||
// vue
|
||||
const app = createApp(App);
|
||||
app.use(createPinia());
|
||||
// ccui.uiElement.setDoc(document);
|
||||
app.use(ccui);
|
||||
app.mount(el);
|
||||
}
|
||||
}
|
||||
export const injectView = new InjectView();
|
||||
38
src/scripts/inject/inspect-list.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export class InspectTarget {
|
||||
private list = [];
|
||||
|
||||
/**
|
||||
* 是否启用过滤
|
||||
*/
|
||||
enabled: boolean = false;
|
||||
addInspectType(item: any) {
|
||||
if (!this.list.find((el) => item === el)) {
|
||||
this.list.push(item);
|
||||
}
|
||||
console.log(this.list);
|
||||
}
|
||||
|
||||
removeInspectType(item: any) {
|
||||
this.list.splice(this.list.indexOf(item), 1);
|
||||
console.log(this.list);
|
||||
}
|
||||
cleanInspectType() {
|
||||
this.list.length = 0;
|
||||
}
|
||||
isContainInspectType(type: any) {
|
||||
return !!this.list.find((el) => type === el);
|
||||
}
|
||||
|
||||
checkNodeComponentsIsInList(node: any) {
|
||||
const comps = node._components;
|
||||
for (let i = 0; i < comps.length; i++) {
|
||||
const comp = comps[i];
|
||||
if (this.list.find((el) => comp instanceof el)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const inspectTarget = new InspectTarget();
|
||||
1050
src/scripts/inject/inspector.ts
Normal file
99
src/scripts/inject/setValue.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { isVersion3 } from "./util";
|
||||
|
||||
interface ConfigItem {
|
||||
path: string[],
|
||||
func: Function;
|
||||
}
|
||||
|
||||
const config: ConfigItem[] = [
|
||||
{
|
||||
path: ["position", "x"],
|
||||
func: (target: any, value: any) => {
|
||||
let pos = target.getPosition();
|
||||
pos.x = value;
|
||||
target.setPosition(pos);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ["position", "y"],
|
||||
func: (target: any, value: any) => {
|
||||
let pos = target.getPosition();
|
||||
pos.y = value;
|
||||
target.setPosition(pos);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ["position", "z"],
|
||||
func: (target: any, value: any) => {
|
||||
let pos = target.getPosition();
|
||||
pos.z = value;
|
||||
target.setPosition(pos);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ["scale", "x"],
|
||||
func: ((target: any, value: any) => {
|
||||
let scale = target.getScale();
|
||||
scale.x = value;
|
||||
target.setScale(scale);
|
||||
})
|
||||
},
|
||||
{
|
||||
path: ["scale", "y"],
|
||||
func: ((target: any, value: any) => {
|
||||
let scale = target.getScale();
|
||||
scale.y = value;
|
||||
target.setScale(scale);
|
||||
})
|
||||
},
|
||||
{
|
||||
path: ["scale", "z"],
|
||||
func: ((target: any, value: any) => {
|
||||
let scale = target.getScale();
|
||||
scale.z = value;
|
||||
target.setScale(scale);
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// 3.x不允许直接设置xyz,需要走setPosition
|
||||
export function trySetValueWithConfig(pathArray: string[], targetObject: any, targetValue: any) {
|
||||
if (isVersion3()) {
|
||||
let fullPath: string = pathArray.toString()
|
||||
let item = config.find(el => {
|
||||
return fullPath.endsWith(el.path.toString())
|
||||
});
|
||||
if (item) {
|
||||
// 将多余的path去掉
|
||||
let leftPathArray = [];
|
||||
let max = pathArray.length - item.path.length;
|
||||
for (let i = 0; i < max; i++) {
|
||||
leftPathArray.push(pathArray[i])
|
||||
}
|
||||
|
||||
let pathObjectValue = getValue(targetObject, leftPathArray);
|
||||
if (pathObjectValue) {
|
||||
try {
|
||||
item.func(pathObjectValue, targetValue);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getValue(target: any, path: string[]) {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
let key = path[i];
|
||||
if (target[key] !== undefined || target.hasOwnProperty(key)) {
|
||||
target = target[key]
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
43
src/scripts/inject/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ArrayData, ImageData, Info, ObjectData, Vec2Data, Vec3Data } from "../../views/devtools/data";
|
||||
|
||||
export interface BuildObjectOptions {
|
||||
path: string[];
|
||||
value: Object;
|
||||
data: ObjectData;
|
||||
}
|
||||
|
||||
export interface BuildArrayOptions {
|
||||
path: string[];
|
||||
value: Object;
|
||||
data: ArrayData;
|
||||
keys: number[];
|
||||
}
|
||||
|
||||
export interface BuildVecOptions {
|
||||
path: string[];
|
||||
keys: Array<{
|
||||
key: string;
|
||||
/**
|
||||
* 分量使用的步进值,优先使用,主要是为了实现不同分量不同的步进
|
||||
*/
|
||||
step?: (key: string) => number;
|
||||
/**
|
||||
* 分量是否可以调整
|
||||
*/
|
||||
disabled?: (key: string, item: Info) => boolean;
|
||||
}>;
|
||||
/**
|
||||
* 所有的vec统一使用的步进值
|
||||
*/
|
||||
step?: number;
|
||||
ctor: Function;
|
||||
value: Object;
|
||||
data: Vec3Data | Vec2Data;
|
||||
}
|
||||
|
||||
export interface BuildImageOptions {
|
||||
path: string[];
|
||||
ctor: Function;
|
||||
value: Object;
|
||||
data: ImageData;
|
||||
}
|
||||
23
src/scripts/inject/util.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
declare const cc: any;
|
||||
|
||||
export function isVersion3() {
|
||||
if (typeof cc.ENGINE_VERSION === "string") {
|
||||
const version: string = cc.ENGINE_VERSION;
|
||||
return version.startsWith("3.")
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isHasProperty(base: Object, key: string): boolean {
|
||||
let ret = Object.getOwnPropertyDescriptor(base, key)
|
||||
if (ret) {
|
||||
return true;
|
||||
} else {
|
||||
let proto = Object.getPrototypeOf(base);
|
||||
if (proto) {
|
||||
return isHasProperty(proto, key)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/scripts/terminal.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
export class Chunk {
|
||||
/**
|
||||
* 显示的值
|
||||
*/
|
||||
value: string = "";
|
||||
/**
|
||||
* 是否换行
|
||||
*/
|
||||
private newline: boolean = false;
|
||||
/**
|
||||
* 显示的样式
|
||||
*/
|
||||
style: string[] = [];
|
||||
constructor(v: string, newline: boolean = false) {
|
||||
this.value = v;
|
||||
this.newline = newline;
|
||||
}
|
||||
color(c: string) {
|
||||
this.style.push(`color:${c}`);
|
||||
return this;
|
||||
}
|
||||
background(c: string) {
|
||||
this.style.push(`background:${c}`);
|
||||
return this;
|
||||
}
|
||||
padding(c: string) {
|
||||
this.style.push(`padding:${c}`);
|
||||
return this;
|
||||
}
|
||||
fontwight(c: string) {
|
||||
this.style.push(`font-weight:${c}`);
|
||||
return this;
|
||||
}
|
||||
bold() {
|
||||
return this.fontwight("bold");
|
||||
}
|
||||
margin(c: string) {
|
||||
this.style.push(`margin:${c}`);
|
||||
return this;
|
||||
}
|
||||
marginLeft(c: string) {
|
||||
this.style.push(`margin-left:${c}`);
|
||||
return this;
|
||||
}
|
||||
marginRight(c: string) {
|
||||
this.style.push(`margin-right:${c}`);
|
||||
return this;
|
||||
}
|
||||
toValue() {
|
||||
return `%c${this.value}${this.newline ? "\n" : ""}`;
|
||||
}
|
||||
toStyle() {
|
||||
return this.style.join(";");
|
||||
}
|
||||
}
|
||||
|
||||
export class Terminal {
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
tag = "terminal";
|
||||
/**
|
||||
* 子标签
|
||||
*/
|
||||
subTag = "";
|
||||
/**
|
||||
* 标签的颜色
|
||||
*/
|
||||
tagColor = "blue";
|
||||
/**
|
||||
* 标签的背景色
|
||||
*/
|
||||
tagBackground = "yellow";
|
||||
/**
|
||||
* 日志文本的颜色
|
||||
*/
|
||||
txtColor = "black";
|
||||
private chunks: Chunk[] = [];
|
||||
constructor(tag: string) {
|
||||
this.tag = tag;
|
||||
}
|
||||
init(): string[] {
|
||||
this.txtColor = "black";
|
||||
this.subTag = "init";
|
||||
return this.log();
|
||||
}
|
||||
|
||||
public log(message: string = "", newline: boolean = false): string[] {
|
||||
const txt = new Chunk(message).color(this.txtColor).background("#e6e6e6").marginLeft("5px");
|
||||
return this.doChunk(newline, [txt]);
|
||||
}
|
||||
public chunkMessage(chunk: Chunk[]) {
|
||||
this.subTag = "message";
|
||||
return this.doChunk(false, chunk);
|
||||
}
|
||||
|
||||
public chunkSend(chunk: Chunk[]) {
|
||||
this.subTag = "send ";
|
||||
return this.doChunk(false, chunk);
|
||||
}
|
||||
private doChunk(newline: boolean = false, chunks: Chunk[]) {
|
||||
this.chunks = [];
|
||||
const tag = new Chunk(this.tag).color(this.tagColor).background(this.tagBackground).padding("0 4px");
|
||||
this.chunks.push(tag);
|
||||
|
||||
const subTag = new Chunk(this.subTag, newline).color(this.tagBackground).background(this.tagColor).padding("0 3px");
|
||||
this.chunks.push(subTag);
|
||||
|
||||
chunks.forEach((c) => {
|
||||
this.chunks.push(c);
|
||||
});
|
||||
|
||||
let head = "*";
|
||||
for (let i = 0; i < this.chunks.length; i++) {
|
||||
const chunk = this.chunks[i];
|
||||
head += chunk.toValue();
|
||||
}
|
||||
const ret = [head];
|
||||
this.chunks.forEach((chunk) => {
|
||||
ret.push(chunk.toStyle());
|
||||
});
|
||||
this.reset();
|
||||
return ret;
|
||||
}
|
||||
private reset() {
|
||||
this.subTag = "";
|
||||
}
|
||||
public blue(message: string): string[] {
|
||||
this.txtColor = "blue";
|
||||
this.subTag = "";
|
||||
return this.log(message);
|
||||
}
|
||||
public green(message: string): string[] {
|
||||
this.txtColor = "green";
|
||||
this.subTag = "";
|
||||
return this.log(message);
|
||||
}
|
||||
public red(message: string): string[] {
|
||||
this.txtColor = "red";
|
||||
this.subTag = "";
|
||||
return this.log(message);
|
||||
}
|
||||
send(msg: string) {
|
||||
this.txtColor = "black";
|
||||
this.subTag = "send";
|
||||
return this.log(`${msg}`);
|
||||
}
|
||||
message(msg: string): string[] {
|
||||
this.txtColor = "black";
|
||||
this.subTag = "message";
|
||||
return this.log(`${msg}`);
|
||||
}
|
||||
connect(msg: string): string[] {
|
||||
this.txtColor = "black";
|
||||
this.subTag = "connect";
|
||||
return this.log(`${msg}`);
|
||||
}
|
||||
disconnect(msg: string): string[] {
|
||||
this.txtColor = "black";
|
||||
this.subTag = "disconnect";
|
||||
return this.log(`${msg}`);
|
||||
}
|
||||
}
|
||||
77
src/views/devtools/bridge.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
import { debugLog, Msg, Page, PluginEvent } from "../../core/types";
|
||||
import { Terminal } from "../../scripts/terminal";
|
||||
import { TestClient, testServer } from "./test/server";
|
||||
export type BridgeCallback = (data: PluginEvent, sender: chrome.runtime.Port) => void;
|
||||
if (chrome.devtools) {
|
||||
console.log("chrome devtools");
|
||||
}
|
||||
class Bridge implements TestClient {
|
||||
private emitter = new TinyEmitter();
|
||||
/**
|
||||
* 和background建立的链接
|
||||
*/
|
||||
private connect: chrome.runtime.Port | null = null;
|
||||
private terminal = new Terminal(Page.Devtools);
|
||||
|
||||
private _inited = false;
|
||||
private init() {
|
||||
if (this._inited) {
|
||||
return;
|
||||
}
|
||||
this._inited = true;
|
||||
if (CCP.Adaptation.Env.isChromeRuntime) {
|
||||
// 调试的标签ID
|
||||
const id = chrome.devtools.inspectedWindow.tabId;
|
||||
this.connect = chrome.runtime.connect({ name: `${Page.Devtools}-${id}` });
|
||||
this.connect.onDisconnect.addListener(() => {
|
||||
debugLog && console.log(...this.terminal.disconnect(""));
|
||||
this.connect = null;
|
||||
});
|
||||
|
||||
this.connect.onMessage.addListener((event, sender: chrome.runtime.Port) => {
|
||||
const data = PluginEvent.create(event);
|
||||
debugLog && console.log(...this.terminal.chunkMessage(data.toChunk()));
|
||||
if (data.valid && data.isTargetDevtools()) {
|
||||
this.emitter.emit(data.msg, data);
|
||||
} else {
|
||||
console.log(JSON.stringify(event));
|
||||
debugger;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
testServer.add(this);
|
||||
}
|
||||
}
|
||||
on(msg: Msg, callback: (data: PluginEvent) => void) {
|
||||
this.emitter.on(msg, callback);
|
||||
}
|
||||
off(msg: Msg, callback: (data: PluginEvent) => void) {
|
||||
this.emitter.off(msg, callback);
|
||||
}
|
||||
recv(event: PluginEvent): void {
|
||||
this.emit(event);
|
||||
}
|
||||
emit(data: PluginEvent) {
|
||||
this.emitter.emit(data.msg, data);
|
||||
}
|
||||
send(msg: Msg, data?: any) {
|
||||
this.init();
|
||||
if (CCP.Adaptation.Env.isChromeDevtools) {
|
||||
if (this.connect) {
|
||||
let sendData = new PluginEvent(Page.Devtools, Page.Background, msg, data);
|
||||
this.connect.postMessage(sendData);
|
||||
} else {
|
||||
console.warn(...this.terminal.log("重新和background建立链接"));
|
||||
this._inited = false;
|
||||
this.init();
|
||||
this.send(msg, data);
|
||||
}
|
||||
} else {
|
||||
testServer.recv(msg, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bridge = new Bridge();
|
||||
15
src/views/devtools/bus.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
|
||||
export enum BusMsg {
|
||||
ShowPlace = "ShowPlace",
|
||||
FoldAllGroup = "FoldAllGroup",
|
||||
UpdateSettings = "UpdateSettings",
|
||||
/**
|
||||
* 开关定时器,方便测试
|
||||
*/
|
||||
EnableSchedule = "EnableSchedule",
|
||||
ChangeContent = "ChangeContent",
|
||||
SelectNode = "SelectNode",
|
||||
}
|
||||
|
||||
export const Bus = new TinyEmitter();
|
||||
274
src/views/devtools/comp/index.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
export enum CompType {
|
||||
Node = "cc.Node",
|
||||
Prefab = "cc.Prefab",
|
||||
Spirte = "cc.Sprite",
|
||||
Label = "cc.Label",
|
||||
Layout = "cc.Layout",
|
||||
Graphics = "cc.Graphics",
|
||||
Widget = "cc.Widget",
|
||||
Camera = "cc.Camera",
|
||||
Canvas = "cc.Canvas",
|
||||
Mask = "cc.Mask",
|
||||
ScrollView = "cc.ScrollView",
|
||||
UITransform = "cc.UITransform",
|
||||
ParticleSystem = "cc.ParticleSystem",
|
||||
EditBox = "cc.EditBox",
|
||||
TiledTile = "cc.TiledTile",
|
||||
Light = "cc.Light",
|
||||
VideoPlayer = "cc.VideoPlayer",
|
||||
MeshRenderer = "cc.MeshRenderer",
|
||||
ProgressBar = "cc.ProgressBar",
|
||||
RichText = "cc.RichText",
|
||||
Slider = "cc.Slider",
|
||||
PageView = "cc.PageView",
|
||||
Webview = "cc.WebView",
|
||||
ToggleGroup = "cc.ToggleGroup",
|
||||
ToggleContainer = "cc.ToggleContainer",
|
||||
Toggle = "cc.Toggle",
|
||||
Button = "cc.Button",
|
||||
BlockInputEvents = "cc.BlockInputEvents",
|
||||
Scene = "cc.Scene",
|
||||
Animation = "cc.Animation",
|
||||
}
|
||||
|
||||
export function getSimpleProperties(typeName: string): string[] {
|
||||
const config = {};
|
||||
config[CompType.Animation] = [
|
||||
"defaultClip", //
|
||||
"clips",
|
||||
"playOnLoad",
|
||||
];
|
||||
config[CompType.Scene] = [
|
||||
"autoReleaseAssets",
|
||||
"position",
|
||||
"scale",
|
||||
"rotation",
|
||||
"color", //
|
||||
];
|
||||
config[CompType.BlockInputEvents] = ["enabled"];
|
||||
config[CompType.Button] = [
|
||||
"target", //
|
||||
"interactable",
|
||||
"enableAutoGrayEffect",
|
||||
"transition",
|
||||
"duration",
|
||||
"zoomScale",
|
||||
"normalColor",
|
||||
"normalSprite",
|
||||
"pressedColor",
|
||||
"pressedSprite",
|
||||
"disabledColor",
|
||||
"disabledSprite",
|
||||
"hoverColor",
|
||||
"hoverSprite",
|
||||
"duration",
|
||||
];
|
||||
config[CompType.Toggle] = [
|
||||
"target",
|
||||
"interactable",
|
||||
"enableAutoGrayEffect",
|
||||
"transition", //
|
||||
"duration",
|
||||
"zoomScale",
|
||||
"isChecked",
|
||||
"checkMark",
|
||||
"toggleGroup",
|
||||
];
|
||||
config[CompType.ToggleContainer] = ["allowSwitchOff"];
|
||||
config[CompType.ToggleGroup] = ["allowSwitchOff"];
|
||||
config[CompType.Webview] = ["url"];
|
||||
config[CompType.PageView] = [
|
||||
"content",
|
||||
"sizeMode",
|
||||
"direction",
|
||||
"scrollThreshold",
|
||||
"autoPageTurningThreshold", //
|
||||
"inertia",
|
||||
"brake",
|
||||
"elastic",
|
||||
"bounceDuration",
|
||||
"indicator",
|
||||
"pageTurningSpeed",
|
||||
"pageTurningEventTiming",
|
||||
"cancelInnerEvents",
|
||||
];
|
||||
config[CompType.Slider] = ["handle", "direction", "progress"];
|
||||
config[CompType.RichText] = [
|
||||
"string",
|
||||
"horizontalAlign",
|
||||
"verticalAlign",
|
||||
"fontSize",
|
||||
"font",
|
||||
"fontFamily",
|
||||
"useSystemFont",
|
||||
"cacheMode",
|
||||
"maxWidth",
|
||||
"lineHeight",
|
||||
"imageAtlas",
|
||||
"handleTouchEvent", //
|
||||
];
|
||||
config[CompType.ProgressBar] = [
|
||||
"barSprite",
|
||||
"mode",
|
||||
"totalLength",
|
||||
"progress",
|
||||
"reverse", //
|
||||
];
|
||||
config[CompType.MeshRenderer] = [
|
||||
"materials", //
|
||||
"mesh",
|
||||
"receiveShadows",
|
||||
"shadowCastingMode",
|
||||
"enableAutoBatch",
|
||||
];
|
||||
config[CompType.VideoPlayer] = [
|
||||
"resourceType",
|
||||
"clip",
|
||||
"currentTime",
|
||||
"volume",
|
||||
"mute",
|
||||
"keepAspectRatio",
|
||||
"isFullscreen",
|
||||
"stayOnBottom", //
|
||||
];
|
||||
config[CompType.Light] = [
|
||||
"type",
|
||||
"color",
|
||||
"intensity", //
|
||||
"range",
|
||||
"shadowType",
|
||||
"range",
|
||||
"spotAngle",
|
||||
"spotExp",
|
||||
];
|
||||
config[CompType.TiledTile] = ["x", "y", "grid"];
|
||||
config[CompType.EditBox] = [
|
||||
"string",
|
||||
"placeholder", //
|
||||
"background",
|
||||
"textLabel",
|
||||
"placeholderLabel",
|
||||
"keyboardReturnType",
|
||||
"inputFlag",
|
||||
"inputMode",
|
||||
"maxLength",
|
||||
"tabIndex",
|
||||
];
|
||||
config[CompType.ParticleSystem] = [
|
||||
"playOnLoad", //
|
||||
"autoRemoveOnFinish",
|
||||
];
|
||||
config[CompType.Node] = [
|
||||
"position", //
|
||||
"rotation",
|
||||
"scale",
|
||||
"anchor",
|
||||
"size",
|
||||
"color",
|
||||
"opacity",
|
||||
"skew",
|
||||
"group",
|
||||
//----------
|
||||
"worldPosition",
|
||||
"worldScale",
|
||||
// "worldRotation",// 渲染有问题,暂时先不支持这个属性
|
||||
];
|
||||
config[CompType.UITransform] = [
|
||||
"anchorPoint", //vec2类型,step会正确处理
|
||||
// "anchor",// FIXME: 会被Inspector属性配对,无法正确处理step
|
||||
"size",
|
||||
];
|
||||
config[CompType.Widget] = [
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"alignMode", //
|
||||
"isAlignLeft",
|
||||
"isAlignRight",
|
||||
"isAlignTop",
|
||||
"isAlignBottom",
|
||||
];
|
||||
config[CompType.Label] = [
|
||||
"string", //
|
||||
"horizontalAlign",
|
||||
"verticalAlign",
|
||||
"fontSize",
|
||||
"lineHeight",
|
||||
"overflow",
|
||||
"font",
|
||||
"fontFamily",
|
||||
"ebableBold",
|
||||
"enableItalic",
|
||||
"enableUnderline",
|
||||
"underlineHeight",
|
||||
"cacheMode",
|
||||
"useSystemFont",
|
||||
];
|
||||
config[CompType.Camera] = ["clearColor", "clearFlags", "cullingMask", "depth", "zoomRatio", "alignWithScreen"];
|
||||
config[CompType.Spirte] = [
|
||||
"atlas",
|
||||
"spriteFrame",
|
||||
"type",
|
||||
"sizeMode", //
|
||||
"fillCenter",
|
||||
"fillRange",
|
||||
"fillRange",
|
||||
"fillStart",
|
||||
"fillType",
|
||||
"grayscale",
|
||||
"color",
|
||||
"spriteAtlas",
|
||||
"trim",
|
||||
"type",
|
||||
];
|
||||
config[CompType.ScrollView] = [
|
||||
"bounceDuration",
|
||||
"content", //
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"inertia",
|
||||
"brake",
|
||||
"elastic",
|
||||
"bounceDuration",
|
||||
"verticalScrollBar",
|
||||
];
|
||||
config[CompType.Mask] = [
|
||||
"alphaThreshold",
|
||||
"inverted",
|
||||
"segments", //
|
||||
"spriteFrame",
|
||||
"type",
|
||||
];
|
||||
config[CompType.Canvas] = [
|
||||
"fitWidth",
|
||||
"fitHeight", //
|
||||
"designResolution",
|
||||
];
|
||||
return config[typeName] || [];
|
||||
}
|
||||
export const VisibleProp = {
|
||||
Active: "active",
|
||||
Enabled: "enabled",
|
||||
};
|
||||
|
||||
export function getNodeIcon(comp: CompType): string {
|
||||
const map = {};
|
||||
map[CompType.Spirte] = "icon_picture";
|
||||
map[CompType.Label] = "icon_text";
|
||||
map[CompType.Node] = "icon_node";
|
||||
map[CompType.Prefab] = "icon_prefab";
|
||||
map[CompType.Animation] = "icon_animation";
|
||||
map[CompType.Button] = "icon_button";
|
||||
map[CompType.EditBox] = "icon_inputbox";
|
||||
map[CompType.Scene] = "icon_cocos";
|
||||
map[CompType.ScrollView] = "icon_scroll_view";
|
||||
map[CompType.Canvas] = "icon_canvas";
|
||||
map[CompType.Camera] = "icon_camera";
|
||||
map[CompType.Mask] = "icon_mask";
|
||||
map[CompType.Widget] = "icon_widget";
|
||||
map[CompType.ProgressBar] = "icon_progress";
|
||||
map[CompType.Layout] = "icon_layout";
|
||||
map[CompType.Graphics] = "icon_graphics";
|
||||
return map[comp] || "";
|
||||
}
|
||||
10
src/views/devtools/const.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const PanelMsg = {
|
||||
Show: "Show",
|
||||
Hide: "Hide",
|
||||
};
|
||||
|
||||
export enum RotateType {
|
||||
None = "none",
|
||||
One = "one",
|
||||
Loop = "loop",
|
||||
}
|
||||
37
src/views/devtools/contextMenu.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
function createPluginMenus() {
|
||||
const menus = [];
|
||||
|
||||
let parent = chrome.contextMenus.create({
|
||||
id: "parent",
|
||||
title: "CC-Inspector",
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: "test",
|
||||
title: "测试右键菜单",
|
||||
parentId: parent,
|
||||
// 上下文环境,可选:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],默认page
|
||||
contexts: ["page"],
|
||||
});
|
||||
chrome.contextMenus.create({
|
||||
id: "notify",
|
||||
parentId: parent,
|
||||
title: "通知",
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(function (info, tab) {
|
||||
if (info.menuItemId === "test") {
|
||||
alert("您点击了右键菜单!");
|
||||
} else if (info.menuItemId === "notify") {
|
||||
chrome.notifications.create("null", {
|
||||
type: "basic",
|
||||
iconUrl: "icons/48.png",
|
||||
title: "通知",
|
||||
message: "测试通知",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chrome.contextMenus.removeAll(() => {
|
||||
createPluginMenus();
|
||||
});
|
||||
694
src/views/devtools/data.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
import { ITreeData } from "@xuyanfeng/cc-ui/types/cc-tree/const";
|
||||
import { v4 } from "uuid";
|
||||
import { getSimpleProperties, VisibleProp } from "./comp";
|
||||
export enum DataType {
|
||||
Number = "Number",
|
||||
String = "String",
|
||||
Text = "Text",
|
||||
Vec2 = "Vec2",
|
||||
Vec3 = "Vec3",
|
||||
Vec4 = "Vec4",
|
||||
Enum = "Enum",
|
||||
Bool = "Bool",
|
||||
Color = "Color",
|
||||
Invalid = "Invalid",
|
||||
Array = "Array", // 暂时在控制台打印下
|
||||
Object = "Object",
|
||||
ObjectCircle = "ObjectCircle",
|
||||
Image = "Image", // 图片
|
||||
Engine = "Engine", // 引擎的类型:cc.Node, cc.Sprite, cc.Label等。。。
|
||||
}
|
||||
|
||||
export class Info {
|
||||
public id: string | null = null;
|
||||
public type: DataType = DataType.Number;
|
||||
public data: any;
|
||||
public readonly: boolean = false;
|
||||
public path: Array<string> = []; // 属性对应的路径
|
||||
public tip: string = "";
|
||||
constructor(id: string = "") {
|
||||
this.id = id || v4();
|
||||
}
|
||||
parse(data: Info) {
|
||||
this.id = data.id;
|
||||
this.tip = data.tip || "";
|
||||
this.path = data.path;
|
||||
this.readonly = data.readonly;
|
||||
}
|
||||
public isEnum(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isVec2(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isVec3(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isVec4(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isBool(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isText(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isString(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isColor(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isInvalid(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isNumber(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isArrayOrObject(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isArray(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isObject(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isObjectCircle(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isImage(): boolean {
|
||||
return false;
|
||||
}
|
||||
public isEngine(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextData extends Info {
|
||||
public data: string = "";
|
||||
constructor(data: string = "") {
|
||||
super();
|
||||
this.type = DataType.Text;
|
||||
this.data = data;
|
||||
}
|
||||
parse(data: TextData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
return this;
|
||||
}
|
||||
public isText(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ObjectItemRequestData {
|
||||
id: string | null;
|
||||
/**
|
||||
* 该对象拥有的所有属性
|
||||
*/
|
||||
data: Property[];
|
||||
}
|
||||
|
||||
export interface FrameDetails {
|
||||
tabID: number;
|
||||
/**
|
||||
* 网页的frameID
|
||||
*/
|
||||
frameID: number;
|
||||
/**
|
||||
* 网页的url
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件里面定义了引擎类型的数据,比如 `@property(cc.Label)`
|
||||
*/
|
||||
export class EngineData extends Info {
|
||||
public engineType: string = "";
|
||||
/**
|
||||
* 组件的uuid
|
||||
*/
|
||||
public engineUUID: string = "";
|
||||
/**
|
||||
* 组件挂在的节点,方便高亮
|
||||
*/
|
||||
public engineNode: string = "";
|
||||
public engineName: string = "";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Engine;
|
||||
}
|
||||
parse(data: EngineData) {
|
||||
super.parse(data);
|
||||
this.engineType = data.engineType;
|
||||
this.engineUUID = data.engineUUID;
|
||||
this.engineName = data.engineName;
|
||||
this.engineNode = data.engineNode;
|
||||
return this;
|
||||
}
|
||||
init(name: string, type: string, uuid: string, node: string = "") {
|
||||
this.engineName = name;
|
||||
this.engineType = type;
|
||||
this.engineUUID = uuid;
|
||||
this.engineNode = node || uuid;
|
||||
return this;
|
||||
}
|
||||
public isEngine(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayData extends Info {
|
||||
data: Array<Property> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Array;
|
||||
}
|
||||
parse(data: ArrayData) {
|
||||
super.parse(data);
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
add(info: Property) {
|
||||
this.data.push(info);
|
||||
return this;
|
||||
}
|
||||
testNormal() {
|
||||
this.add(new Property("arr-text", new TextData("text")));
|
||||
this.add(new Property("arr-bool", new BoolData(true)));
|
||||
this.add(new Property("arr-number", new NumberData(100)));
|
||||
return this;
|
||||
}
|
||||
testObject() {
|
||||
this.add(new Property("obj", new ObjectData().testNormal()));
|
||||
return this;
|
||||
}
|
||||
testArray() {
|
||||
this.add(new Property("arr", new ArrayData().testNormal()));
|
||||
return this;
|
||||
}
|
||||
testSub() {
|
||||
this.add(new Property("text", new TextData("text")));
|
||||
const sub = new ArrayData();
|
||||
sub.add(new Property("string", new StringData("sub")));
|
||||
this.add(new Property("array", sub));
|
||||
return this;
|
||||
}
|
||||
public isArray(): boolean {
|
||||
return true;
|
||||
}
|
||||
public isArrayOrObject(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export class ObjectCircleData extends Info {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.ObjectCircle;
|
||||
}
|
||||
parse(data: ObjectCircleData) {
|
||||
super.parse(data);
|
||||
return this;
|
||||
}
|
||||
public isObjectCircle(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export class ObjectData extends Info {
|
||||
/**
|
||||
* 对于object无限递归的情况,会自动中断
|
||||
* 本质上和Array区别不是很大
|
||||
*/
|
||||
data: Array<Property> = [];
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Object;
|
||||
}
|
||||
parse(data: ObjectData) {
|
||||
super.parse(data);
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
add(info: Property) {
|
||||
this.data.push(info);
|
||||
return this;
|
||||
}
|
||||
testNormal() {
|
||||
this.add(new Property("obj-text", new TextData("text")));
|
||||
this.add(new Property("obj-bool", new BoolData(true)));
|
||||
this.add(new Property("obj-number", new NumberData(100)));
|
||||
return this;
|
||||
}
|
||||
testObject() {
|
||||
this.add(new Property("obj", new ObjectData().testNormal()));
|
||||
return this;
|
||||
}
|
||||
testArray() {
|
||||
this.add(new Property("array", new ArrayData().testNormal()));
|
||||
return this;
|
||||
}
|
||||
public isObject(): boolean {
|
||||
return true;
|
||||
}
|
||||
public isArrayOrObject(): boolean {
|
||||
return true;
|
||||
}
|
||||
public getInfo() {
|
||||
return `${this.id}: ${this.path.join("/")}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidData extends Info {
|
||||
data: "undefined" | "null" | "Infinity" | "NaN" | string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.type = DataType.Invalid;
|
||||
}
|
||||
parse(data: InvalidData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
return this;
|
||||
}
|
||||
public isInvalid(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class ColorData extends Info {
|
||||
public data: string = "#ffffff";
|
||||
constructor(color: string = "#ffffff") {
|
||||
super();
|
||||
this.type = DataType.Color;
|
||||
this.data = color;
|
||||
}
|
||||
parse(data: ColorData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
return this;
|
||||
}
|
||||
public isColor(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class StringData extends Info {
|
||||
public data: string = "";
|
||||
constructor(data: string = "") {
|
||||
super();
|
||||
this.type = DataType.String;
|
||||
this.data = data;
|
||||
}
|
||||
parse(data: StringData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
return this;
|
||||
}
|
||||
public isString(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberData extends Info {
|
||||
public data: number = 0;
|
||||
public step: number = 1;
|
||||
/**
|
||||
* 是否禁用,因为cc.Widget组件会影响调整x、y
|
||||
*/
|
||||
public disabled: boolean = false;
|
||||
constructor(data: number = 0) {
|
||||
super();
|
||||
this.type = DataType.Number;
|
||||
this.data = data;
|
||||
}
|
||||
parse(data: NumberData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
this.step = data.step || 1;
|
||||
this.disabled = !!data.disabled;
|
||||
return this;
|
||||
}
|
||||
public isNumber(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoolData extends Info {
|
||||
public data: boolean = false;
|
||||
constructor(bol: boolean = false) {
|
||||
super();
|
||||
this.type = DataType.Bool;
|
||||
this.data = bol;
|
||||
}
|
||||
parse(data: BoolData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
return this;
|
||||
}
|
||||
public isBool(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Vec2Data extends Info {
|
||||
data: Array<Property> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Vec2;
|
||||
this.data = [];
|
||||
return this;
|
||||
}
|
||||
parse(data: Vec2Data) {
|
||||
super.parse(data);
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
add(info: Property) {
|
||||
this.data.push(info);
|
||||
return this;
|
||||
}
|
||||
test() {
|
||||
this.add(new Property("x", new NumberData(100)));
|
||||
this.add(new Property("y", new NumberData(200)));
|
||||
return this;
|
||||
}
|
||||
public isVec2(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Vec3Data extends Info {
|
||||
data: Array<Property> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Vec3;
|
||||
this.data = [];
|
||||
return this;
|
||||
}
|
||||
parse(data: Vec3Data) {
|
||||
super.parse(data);
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
add(info: Property) {
|
||||
this.data.push(info);
|
||||
return this;
|
||||
}
|
||||
test() {
|
||||
this.add(new Property("x", new NumberData(100)));
|
||||
this.add(new Property("y", new NumberData(200)));
|
||||
this.add(new Property("z", new NumberData(300)));
|
||||
return this;
|
||||
}
|
||||
public isVec3(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export class Vec4Data extends Info {
|
||||
data: Array<Property> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Vec4;
|
||||
this.data = [];
|
||||
return this;
|
||||
}
|
||||
parse(data: Vec4Data) {
|
||||
super.parse(data);
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
add(info: Property) {
|
||||
this.data.push(info);
|
||||
return this;
|
||||
}
|
||||
test() {
|
||||
this.add(new Property("x", new NumberData(100)));
|
||||
this.add(new Property("y", new NumberData(200)));
|
||||
this.add(new Property("z", new NumberData(300)));
|
||||
this.add(new Property("w", new NumberData(400)));
|
||||
return this;
|
||||
}
|
||||
public isVec4(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export class ImageData extends Info {
|
||||
/**
|
||||
* 图片的url路径
|
||||
*/
|
||||
data: string = "";
|
||||
desc: string = "";
|
||||
constructor() {
|
||||
super();
|
||||
this.type = DataType.Image;
|
||||
this.data = "";
|
||||
this.desc = "";
|
||||
return this;
|
||||
}
|
||||
parse(data: ImageData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
this.desc = data.desc;
|
||||
return this;
|
||||
}
|
||||
test() {
|
||||
const cocos =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABz1JREFUaEO9WXuMHVUZ/32zfbHSQkDa3pkukALb7N6ZUjFGjBFjopCUBA1PRYyJaVIVMVTF3rlbhAh7z922EB8oVRKNlUcgSAwCMWAQlKg0msjOzPYFwdCdMy3U2tqy7Xbvzmfm7t7dO/fO7Jx799b5c77X73fOd77znXMIHfx04e6TttnbQZeZrihTowWFXMnbSRqulIX8/41ERwkYD7zVyxMn94B5WNrWuha4t63aUQIRCr3svQfmDzLwWmCbn2gbmaJhxwnkhrznKeT1UXxmeiEo5q9VxNKWWucJDA6/SpoWjXzVtwbtiVG7/9a20CkYdZSAIbztDP4SwEsBOmsmPk8+LIuXf10BT8sqHSPQUx65qsL8cnCJtxQ333zSEO5pBhbWEBHhfr9g3t0ywgyDjhBY9eBfzgrHlx0i0FHfzl8YxTSEV2KwXR9/krHpUNH8QSdJdISAUXLfZPAlXUsnjAN3XCFrAHXh+QDrdYAZC7puk3f1Pd4pEvMmkCt5zxPxegYOBraZiwH72d+79f8sOQGeWtDVj7mCicp6ec+HXuoEiXkRWFX2CiGziICMn72w7993rNnTCEoveb8H8TXRf6pFYzpVCSsfOTRwuTtfEm0TuFDsW13B6b0AFgD0nrTzy9PAGCX3fSZ018sZON61+JyLR7/Vc6T+v36/c4vcYj2pSqxtAkbJOcpE51Rr/YJFV47e1ft6WtAVg7tv7dImH2uW02Fp5y+o/58Tzh8D2/rUGSWgC2cEoL5qEKLDshAHkRRcF85+gC5NkI1K2+yZXfjuCWmbZ58xAnrJeRREX6wF6OpafM2B7172okpAXbiT1c256aN90s6vWbl9f782Me5J21TODGXFKObyoeHPLWB6Bjy9HBlHZNE8XwV8pGOUnB1MtDFZn97gEH8ljb/KpN0ZFPp/qOI3kUDP1v1XJ42qLpyx+haBuXJDUFz3jEqgmk792onZVZHQu2BeziHvCgasj6r4TSSgl71/yEL+w7HqINy3AVxc+0fgY75tnasSpF7nvAd29Sw53f1Oqh0DDJwIiuZSFd+JBHIl91hQNKsVJvp04TwHULwt1sKvyM1rf6kSpFEnJ7zXCPzxdFuCHO9fiHupkuU/lQBxZZMcWPeLlWX3do35oenueMof0XFZyC/Lcj6XXBfuxNQekvyF4B0HbetrWTFSCWiEYzx+3hW05Ihknu0qownmEHcGA5bSIksDkBMjGwjhI2lyIrziF8zM/SB9BoiWgVBdVPVBiHnML1ofyBoZFbkunFGAjERd5rdl0Vqd5WcOAlg2lTYc80EIv+fba+/Lcqwq14UbDzBtqFok5ibQgJ/BpwLbmj1pqaKcQ88Qw79maLc1qRAmZMFclBUiuYwK9wCAVU3GHG6VxbWbs5y2KjeEO8ZAfGAILAtmwq4d9548A8L9EwHTVyIz03Ba2ubiVsGp6Otl77dg/mwjNGnnMzuFZAKDziuk0SdjDhk/kUXzGyqAWtXJCfcgASua7LRFV8vNvXMefJQIEGHCV8jHVoHX9PWyVwFzV5N9iCE5YBbm8qtGABjzbbMjpbMRzKU/2r947P3xU0kgCfyCb1tzXowpEYicV8Lule8OrD7U7iin2eWE+yABm5LkDHICO7923jMQOSCiXX4hr9QhtkJSF94egNck2oQI5IBZvdXIDf3z+mBzc+ebUoW8pwh8U4PTM1KFdOFG6ZNc3QjHu/13LhjTL3qdWNvtF/u+0Eg0eR8ouQ+BcHv1GoFnN0oKsdEfMH/eyghn6erC4VijWGfAQIU4ul4Fpe0JiQQM4d7NwPcbgzNjJCia+SxQqnJj0LmWNXquWX/m/mVaxH+TtvWx5IWe8De3dWQDTSZ0iqRNykJ/agusCrymlys7TxPTDRl2FWmbM3esSilklJ0iQIN12TNjx4SdQcH8cqtgk/R14f0L4IuqsihTE/KBQE/6dv7zafFSt+pc2b2PGFuaDBn/lXWntfkQMYR7koElVR/NjW908tgRZBxq5uw19LK3DczfaQSpTeLG0S3mb+YDPrLVhTcJcGLDxuCdgW1lznRms6QL98cAGnug3dI2+9slYAztuZHDyk40dqA1h0RPyUL+FhX/mQSqI1Xyfgri2fMp81ECXvaLVtYCbMKgl4b3grTexJypZhL9zrfz16mAr2Wekq5ecraBKJ5OzBUCnlUhogtnOxF9M36+jocm8B982/qMEqBpJaUZqDnUB517odE9zQGqRB73E3I2N7Svj8LxFwGKH5Aaqg4Dfw5s86pWwLc0AzMkhLMdoG8nB+IKCL+SBWvDVOo5L4Ho01mgmGlXUGyvz2ppBmpAjLK3g5k3phZvILqQGgeg0oK/IW2z7Vf9tghMja77GAhN779N5Txlg5oejHlVs7ZSqD4d9JI7/XxEaUVlVr2eSPVym9+SBTPpvSAr42Lytmcg8rJi25vLuyZO7gVRi5e8fFjaVuxlpiXUdcr/A3Hqok9HdvAQAAAAAElFTkSuQmCC";
|
||||
this.data = cocos;
|
||||
return this;
|
||||
}
|
||||
public isImage(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class EnumData extends Info {
|
||||
public data: string | number = 0;
|
||||
public values: Array<{ name: string; value: any }> = [];
|
||||
constructor(data: string | number = 0) {
|
||||
super();
|
||||
this.type = DataType.Enum;
|
||||
this.data = data;
|
||||
}
|
||||
parse(data: EnumData) {
|
||||
super.parse(data);
|
||||
this.data = data.data;
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
const item = data.values[i];
|
||||
this.values.push({ name: item.name, value: item.value });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public isEnum(): boolean {
|
||||
return this.type === DataType.Enum;
|
||||
}
|
||||
test() {
|
||||
this.values.push({ name: "1", value: 1 });
|
||||
this.values.push({ name: "2", value: 2 });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TreeData implements ITreeData {
|
||||
id: string = "";
|
||||
icon: string = "";
|
||||
color: string = "";
|
||||
active: boolean = true;
|
||||
text: string = "";
|
||||
children: TreeData[] = [];
|
||||
constructor(id: string = "", text: string = "") {
|
||||
this.id = id;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
export class Property {
|
||||
public name: string = "property";
|
||||
public value: Info = new Info();
|
||||
|
||||
constructor(name: string, info: Info) {
|
||||
this.name = name;
|
||||
this.value = info;
|
||||
}
|
||||
parse(data: Property) {
|
||||
this.name = data.name;
|
||||
this.value = data.value;
|
||||
switch (data.value.type) {
|
||||
case DataType.Object:
|
||||
this.value = new ObjectData().parse(data.value as ObjectData);
|
||||
break;
|
||||
case DataType.Number:
|
||||
this.value = new NumberData().parse(data.value as NumberData);
|
||||
break;
|
||||
case DataType.Array:
|
||||
this.value = new ArrayData().parse(data.value as ArrayData);
|
||||
break;
|
||||
case DataType.String:
|
||||
this.value = new StringData().parse(data.value as StringData);
|
||||
break;
|
||||
case DataType.Bool:
|
||||
this.value = new BoolData().parse(data.value as BoolData);
|
||||
break;
|
||||
case DataType.Color:
|
||||
this.value = new ColorData().parse(data.value as ColorData);
|
||||
break;
|
||||
case DataType.Vec2:
|
||||
this.value = new Vec2Data().parse(data.value as Vec2Data);
|
||||
break;
|
||||
case DataType.Vec3:
|
||||
this.value = new Vec3Data().parse(data.value as Vec3Data);
|
||||
break;
|
||||
case DataType.Vec4:
|
||||
this.value = new Vec4Data().parse(data.value as Vec4Data);
|
||||
break;
|
||||
case DataType.Image:
|
||||
this.value = new ImageData().parse(data.value as ImageData);
|
||||
break;
|
||||
case DataType.Text:
|
||||
this.value = new TextData().parse(data.value as TextData);
|
||||
break;
|
||||
case DataType.Enum:
|
||||
this.value = new EnumData().parse(data.value as EnumData);
|
||||
break;
|
||||
case DataType.Engine:
|
||||
this.value = new EngineData().parse(data.value as EngineData);
|
||||
break;
|
||||
case DataType.Invalid:
|
||||
this.value = new InvalidData(data.value).parse(data.value as InvalidData);
|
||||
break;
|
||||
case DataType.ObjectCircle:
|
||||
this.value = new ObjectCircleData().parse(data.value as ObjectCircleData);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`not support type: ${typeof data === "string" ? data : JSON.stringify(data)}`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Group {
|
||||
/**
|
||||
* 节点的UUID
|
||||
*/
|
||||
public id: string = "";
|
||||
public name: string = "group";
|
||||
public data: Array<Property> = [];
|
||||
|
||||
constructor(name: string, id?: string) {
|
||||
this.name = name;
|
||||
this.id = id || "";
|
||||
}
|
||||
isSimple(name: string): boolean {
|
||||
if (name === VisibleProp.Active || name === VisibleProp.Enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const arr = getSimpleProperties(this.name);
|
||||
if (arr.length) {
|
||||
const b = arr.find((el) => el === name);
|
||||
return !!b;
|
||||
} else {
|
||||
// 这个类型,没有追加精简属性的配置,默认都是精简属性
|
||||
return true;
|
||||
}
|
||||
}
|
||||
parse(data: Group, simple: boolean = false) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.data = [];
|
||||
for (let i = 0; i < data.data.length; i++) {
|
||||
const item = data.data[i];
|
||||
if (simple && !this.isSimple(item.name)) {
|
||||
continue;
|
||||
}
|
||||
const property = new Property(item.name, item.value).parse(item);
|
||||
this.data.push(property);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
addProperty(property: Property) {
|
||||
this.data.push(property);
|
||||
}
|
||||
buildProperty(name: string, info: Info) {
|
||||
info.path.push(name);
|
||||
const property = new Property(name, info);
|
||||
this.addProperty(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
sort() {
|
||||
let order = ["name", "active", "enabled", "uuid", "position", "rotation", "scale", "anchor", "size", "color", "opacity", "skew", "group"];
|
||||
let orderKeys: Array<Property> = [];
|
||||
let otherKeys: Array<Property> = [];
|
||||
this.data.forEach((property) => {
|
||||
if (order.find((el) => el === property.name)) {
|
||||
orderKeys.push(property);
|
||||
} else {
|
||||
otherKeys.push(property);
|
||||
}
|
||||
});
|
||||
orderKeys.sort((a, b) => {
|
||||
return order.indexOf(a.name) - order.indexOf(b.name);
|
||||
});
|
||||
otherKeys.sort();
|
||||
this.data = orderKeys.concat(otherKeys);
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeInfoData {
|
||||
/**
|
||||
* 节点的uuid
|
||||
*/
|
||||
uuid: string;
|
||||
/**
|
||||
* 组件数据
|
||||
*/
|
||||
group: Group[];
|
||||
constructor(uuid: string, group: Group[]) {
|
||||
this.uuid = uuid;
|
||||
this.group = group;
|
||||
}
|
||||
/**
|
||||
* 将json数据解析成NodeInfoData
|
||||
*/
|
||||
parse(data: NodeInfoData, simple: boolean = false) {
|
||||
this.uuid = data.uuid;
|
||||
this.group = [];
|
||||
for (let i = 0; i < data.group.length; i++) {
|
||||
const item = data.group[i];
|
||||
const group = new Group(item.name, item.id).parse(item, simple);
|
||||
this.group.push(group);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
58
src/views/devtools/find.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="find-game">
|
||||
<div style="display: flex; flex-direction: column; margin-bottom: 3px">
|
||||
<span>no games created by cocos creator found!</span>
|
||||
<span>{{ msg }}</span>
|
||||
</div>
|
||||
<Refresh @click="onBtnClickUpdatePage"></Refresh>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { Msg, PluginEvent, ResponseSupportData } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
import { GA_Button } from "../../ga/type";
|
||||
import { bridge } from "./bridge";
|
||||
import Refresh from "./refresh.vue";
|
||||
import { checkSupport } from "./util";
|
||||
export default defineComponent({
|
||||
name: "find",
|
||||
components: { Refresh },
|
||||
setup(props) {
|
||||
bridge.on(Msg.ResponseSupport, (event: PluginEvent) => {
|
||||
let data: ResponseSupportData = event.data;
|
||||
const b: boolean = data.support;
|
||||
if (b) {
|
||||
msg.value = "";
|
||||
} else {
|
||||
msg.value = data.msg;
|
||||
}
|
||||
});
|
||||
const msg = ref<string>("");
|
||||
return {
|
||||
msg,
|
||||
onBtnClickUpdatePage() {
|
||||
ga.clickButton(GA_Button.FreshManual);
|
||||
checkSupport();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.find-game {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
|
||||
span {
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/views/devtools/game-info.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="game-info">
|
||||
<CCSection name="DynamicAtals">
|
||||
<template v-slot:header>
|
||||
<div style="flex: 1"></div>
|
||||
<div v-if="supportView" @click="onClickAtlasView" title="show dynamic atlas in game" class="iconfont view" :class="showDynamicAtals ? 'icon_view_on' : 'icon_view_off'"></div>
|
||||
</template>
|
||||
<CCProp name="enable" align="left"> <CCCheckBox :disabled="true" :value="dynamicAtalsEnable"></CCCheckBox> </CCProp>
|
||||
<CCProp name="texture bleeding" align="left"> <CCCheckBox :disabled="true" :value="dynamicAtalsTextureBleeding"></CCCheckBox> </CCProp>
|
||||
<CCProp name="atlas count" align="left"> <CCInputNumber :disabled="true" :value="dynamicAtalsCount"></CCInputNumber> </CCProp>
|
||||
<CCProp name="max atlas count" align="left"> <CCInputNumber :disabled="true" :value="dynamicAtalsMaxAtlasCount"></CCInputNumber> </CCProp>
|
||||
<CCProp name="max frame size" align="left"> <CCInputNumber :disabled="true" :value="dynamicAtalsMaxFrameSize"></CCInputNumber> </CCProp>
|
||||
<CCProp name="atlas texture size" align="left"> <CCInputNumber :disabled="true" :value="dynamicAtalsTextureSize"></CCInputNumber> </CCProp>
|
||||
</CCSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, onMounted, onUnmounted, ref, toRaw } from "vue";
|
||||
import { Msg, PluginEvent, ResponseGameInfoData } from "../../core/types";
|
||||
import { bridge } from "./bridge";
|
||||
const { CCProp, CCCheckBox, CCSection, CCInputNumber } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "game-info",
|
||||
components: { CCProp, CCCheckBox, CCSection, CCInputNumber },
|
||||
setup() {
|
||||
function onGameInfo(data: PluginEvent) {
|
||||
const gameInfo = data.data as ResponseGameInfoData;
|
||||
dynamicAtalsEnable.value = !!gameInfo.dynamicAtals.enable;
|
||||
dynamicAtalsCount.value = gameInfo.dynamicAtals.atlasCount;
|
||||
dynamicAtalsMaxAtlasCount.value = gameInfo.dynamicAtals.maxAtlasCount;
|
||||
dynamicAtalsMaxFrameSize.value = gameInfo.dynamicAtals.maxFrameSize;
|
||||
dynamicAtalsTextureSize.value = gameInfo.dynamicAtals.textureSize;
|
||||
dynamicAtalsTextureBleeding.value = gameInfo.dynamicAtals.textureBleeding;
|
||||
supportView.value = !!gameInfo.dynamicAtals.supportView;
|
||||
}
|
||||
function onDynamicAtlasView(data: PluginEvent) {
|
||||
const b = data.data as boolean;
|
||||
showDynamicAtals.value = b;
|
||||
}
|
||||
onMounted(() => {
|
||||
bridge.on(Msg.ResponseGameInfo, onGameInfo);
|
||||
bridge.on(Msg.ResponseDynamicAtlasView, onDynamicAtlasView);
|
||||
bridge.send(Msg.RequestGameInfo);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
bridge.off(Msg.ResponseDynamicAtlasView, onDynamicAtlasView);
|
||||
bridge.off(Msg.ResponseGameInfo, onGameInfo);
|
||||
});
|
||||
const dynamicAtalsEnable = ref(true);
|
||||
const dynamicAtalsCount = ref(0);
|
||||
const dynamicAtalsMaxAtlasCount = ref(0);
|
||||
const dynamicAtalsMaxFrameSize = ref(0);
|
||||
const dynamicAtalsTextureSize = ref(0);
|
||||
const dynamicAtalsTextureBleeding = ref(true);
|
||||
const showDynamicAtals = ref(false);
|
||||
const supportView = ref(false);
|
||||
function onClickAtlasView() {
|
||||
const b = toRaw(showDynamicAtals.value);
|
||||
bridge.send(Msg.RequestDynamicAtlasView, !b);
|
||||
}
|
||||
return {
|
||||
supportView,
|
||||
showDynamicAtals,
|
||||
dynamicAtalsEnable,
|
||||
dynamicAtalsCount,
|
||||
dynamicAtalsMaxAtlasCount,
|
||||
dynamicAtalsMaxFrameSize,
|
||||
dynamicAtalsTextureSize,
|
||||
dynamicAtalsTextureBleeding,
|
||||
onClickAtlasView,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.game-info {
|
||||
background-color: rgb(32, 32, 32);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.view {
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
font-size: 22px;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
&:active {
|
||||
color: rgb(255, 153, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
432
src/views/devtools/hierarchy.vue
Normal file
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<div class="left">
|
||||
<CCDock name="Hierarchy">
|
||||
<template v-slot:tab-name-before> </template>
|
||||
<template v-slot:title>
|
||||
<Refresh @click="onClickRefresh" :type="rotateType"></Refresh>
|
||||
<div class="engine-version" v-if="engineVersion">Cocos Creator V{{ engineVersion }}</div>
|
||||
</template>
|
||||
<CCInput style="flex: none" placeholder="enter keywords to filter" :data="filterText" v-if="false">
|
||||
<slot>
|
||||
<i class="matchCase iconfont icon_font_size" @click.stop="onChangeCase" title="match case" :style="{ color: matchCase ? 'red' : '' }"></i>
|
||||
</slot>
|
||||
</CCInput>
|
||||
<CCTree :show-icon="config.showTreeIcon" @do-search="doSearch" :search="true" @node-menu="onMenu" @contextmenu.prevent.stop="onMenu" style="flex: 1" ref="elTree" :expand-keys="expandedKeys" :default-expand-all="false" :value="treeData" @node-expand="onNodeExpand" @node-collapse="onNodeCollapse" @node-click="handleNodeClick" @node-unclick="handleNodeUnclick" @node-enter="handleNodeEnter" @node-leave="handleNodeLeave"></CCTree>
|
||||
</CCDock>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { IUiMenuItem } from "@xuyanfeng/cc-ui/types/cc-menu/const";
|
||||
import { HandExpandOptions } from "@xuyanfeng/cc-ui/types/cc-tree/const";
|
||||
import Mousetrap, { MousetrapInstance } from "mousetrap";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { defineComponent, nextTick, onMounted, onUnmounted, ref, toRaw, watch } from "vue";
|
||||
import { Msg, PluginEvent, RequestTreeInfoData, RequestUseFrameData, ResponseSetPropertyData, ResponseSupportData } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { bridge } from "./bridge";
|
||||
import { Bus, BusMsg } from "./bus";
|
||||
import { RotateType } from "./const";
|
||||
import { EngineData, TreeData } from "./data";
|
||||
import GameInfo from "./game-info.vue";
|
||||
import Refresh from "./refresh.vue";
|
||||
import { appStore } from "./store";
|
||||
import { Timer } from "./timer";
|
||||
const { CCTree, CCFootBar, CCDock, CCDialog, CCInput, CCButton, CCInputNumber, CCSelect, CCButtonGroup, CCCheckBox, CCColor, CCDivider } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "hierarchy",
|
||||
components: { Refresh, CCButtonGroup, CCInput, CCTree, CCDock },
|
||||
setup() {
|
||||
const funcShowPlace = (data: EngineData) => {
|
||||
console.log(data);
|
||||
_expand(data.engineNode);
|
||||
};
|
||||
const funcEnableSchedule = (b: boolean) => {
|
||||
if (b) {
|
||||
timer.create();
|
||||
} else {
|
||||
timer.clean();
|
||||
}
|
||||
};
|
||||
const timer: Timer = new Timer();
|
||||
timer.onWork = () => {
|
||||
rotateType.value = RotateType.Loop;
|
||||
config.value.refreshHirarchy = true;
|
||||
appStore().save();
|
||||
updateTree();
|
||||
};
|
||||
timer.onClean = () => {
|
||||
rotateType.value = RotateType.None;
|
||||
config.value.refreshHirarchy = false;
|
||||
appStore().save();
|
||||
};
|
||||
timer.name = "hierarchy";
|
||||
let ins: MousetrapInstance | null = null;
|
||||
function onQuickVisible() {
|
||||
ga.fireEvent(GA_EventName.SpaceVisible);
|
||||
console.log("onQuickVisible");
|
||||
if (selectedUUID) {
|
||||
bridge.send(Msg.RequestVisible, selectedUUID);
|
||||
}
|
||||
}
|
||||
function changeContent(data: RequestUseFrameData) {
|
||||
treeData.value = [];
|
||||
selectedUUID = null;
|
||||
}
|
||||
function onInspectNode(data: PluginEvent) {
|
||||
const uuid = data.data as string;
|
||||
if (!uuid) {
|
||||
return;
|
||||
}
|
||||
updateSelect(uuid);
|
||||
nextTick(() => {
|
||||
if (elTree.value) {
|
||||
elTree.value.handExpand(uuid, { highlight: true, select: true, scroll: true } as HandExpandOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
if (elTree.value) {
|
||||
const el = toRaw(elTree.value);
|
||||
ins = new Mousetrap(el.treeElement);
|
||||
ins.bind(["space"], onQuickVisible, "keydown");
|
||||
}
|
||||
Bus.on(BusMsg.ChangeContent, changeContent);
|
||||
Bus.on(BusMsg.ShowPlace, funcShowPlace);
|
||||
Bus.on(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
bridge.on(Msg.InspectNode, onInspectNode);
|
||||
if (config.value.refreshHirarchy) {
|
||||
timer.create(true);
|
||||
} else {
|
||||
updateTree();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (ins) {
|
||||
ins.unbind(["space"], "keydown");
|
||||
}
|
||||
Bus.off(BusMsg.ChangeContent, changeContent);
|
||||
Bus.off(BusMsg.ShowPlace, funcShowPlace);
|
||||
Bus.off(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
bridge.off(Msg.InspectNode, onInspectNode);
|
||||
timer.clean();
|
||||
});
|
||||
function _expand(uuid: string) {
|
||||
if (elTree.value) {
|
||||
elTree.value.handExpand(uuid, { highlight: true });
|
||||
}
|
||||
}
|
||||
function updateTree() {
|
||||
console.log("update tree info");
|
||||
const id = toRaw(frameID.value);
|
||||
bridge.send(Msg.RequstTreeInfo, { frameID: id } as RequestTreeInfoData);
|
||||
}
|
||||
|
||||
function updateFilterText(val: any) {
|
||||
(elTree.value as any)?.filter(val);
|
||||
}
|
||||
const filterText = ref<string>("");
|
||||
watch(filterText, (val) => {
|
||||
// TODO: 过滤树
|
||||
updateFilterText(val);
|
||||
});
|
||||
const { config, frameID } = storeToRefs(appStore());
|
||||
const matchCase = ref<boolean>(false);
|
||||
const elTree = ref<typeof CCTree>(null);
|
||||
const treeData = ref<TreeData[]>([]);
|
||||
let selectedUUID: string | null = null;
|
||||
const engineVersion = ref("");
|
||||
bridge.on(Msg.ResponseSupport, (event: PluginEvent) => {
|
||||
let data: ResponseSupportData = event.data;
|
||||
const isCocosGame: boolean = data.support;
|
||||
if (isCocosGame) {
|
||||
engineVersion.value = data.version;
|
||||
} else {
|
||||
engineVersion.value = "";
|
||||
}
|
||||
});
|
||||
bridge.on(Msg.ResponseTreeInfo, (event: PluginEvent) => {
|
||||
let data: Array<TreeData> = event.data;
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
treeData.value = data;
|
||||
nextTick(() => {
|
||||
if (elTree.value) {
|
||||
elTree.value.handChoose(selectedUUID);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bridge.on(Msg.ResponseSetProperty, (event: PluginEvent) => {
|
||||
let data: ResponseSetPropertyData = event.data;
|
||||
const uuid = data.path[0];
|
||||
const key = data.path[1];
|
||||
const value = data.data;
|
||||
let treeArray: Array<TreeData> = [];
|
||||
|
||||
function circle(array: Array<TreeData>) {
|
||||
array.forEach((item) => {
|
||||
treeArray.push(item);
|
||||
circle(item.children);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新指定uuid节点的tree的name
|
||||
circle(treeData.value);
|
||||
let ret = treeArray.find((el) => el.id === uuid);
|
||||
if (ret) {
|
||||
if (key === "name") {
|
||||
ret.text = value;
|
||||
}
|
||||
if (key === "active") {
|
||||
ret.active = !!value;
|
||||
}
|
||||
}
|
||||
});
|
||||
const expandedKeys = ref<Array<string>>([]);
|
||||
function updateSelect(uuid: string | null) {
|
||||
selectedUUID = uuid;
|
||||
Bus.emit(BusMsg.SelectNode, uuid);
|
||||
if (config.value.clickInspect) {
|
||||
bridge.send(Msg.SelectNode, uuid);
|
||||
}
|
||||
}
|
||||
const rotateType = ref<RotateType>(RotateType.None);
|
||||
if (config.value.refreshHirarchy) {
|
||||
rotateType.value = RotateType.Loop;
|
||||
}
|
||||
let preSearch = "";
|
||||
return {
|
||||
config,
|
||||
doSearch(v: string) {
|
||||
if (v && preSearch !== v) {
|
||||
ga.fireEventWithParam(GA_EventName.TreeSearch, v);
|
||||
preSearch = v;
|
||||
}
|
||||
},
|
||||
onClickRefresh() {
|
||||
updateTree();
|
||||
},
|
||||
rotateType,
|
||||
engineVersion,
|
||||
expandedKeys,
|
||||
elTree,
|
||||
filterText,
|
||||
treeData,
|
||||
matchCase,
|
||||
frameID,
|
||||
handleNodeUnclick() {
|
||||
updateSelect(null);
|
||||
},
|
||||
handleNodeEnter(data: TreeData | null) {
|
||||
if (!config.value.hoverInspect) {
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
bridge.send(Msg.HoverNode, data.id);
|
||||
}
|
||||
},
|
||||
handleNodeLeave(data: TreeData | null) {
|
||||
if (data) {
|
||||
bridge.send(Msg.HoverNode, null);
|
||||
}
|
||||
},
|
||||
handleNodeClick(data: TreeData | null) {
|
||||
if (data) {
|
||||
updateSelect(data.id);
|
||||
} else {
|
||||
updateSelect(null);
|
||||
}
|
||||
},
|
||||
|
||||
onNodeExpand(data: TreeData) {
|
||||
if (data.id) {
|
||||
expandedKeys.value.push(data.id);
|
||||
ga.fireEventWithParam(GA_EventName.Hierarchy, "node expand");
|
||||
}
|
||||
},
|
||||
onNodeCollapse(data: TreeData) {
|
||||
if (data.id) {
|
||||
ga.fireEventWithParam(GA_EventName.Hierarchy, "node collapse");
|
||||
const keys = toRaw(expandedKeys.value);
|
||||
const index = keys.findIndex((el) => el === data.id);
|
||||
if (index !== -1) {
|
||||
keys.splice(index, 1);
|
||||
}
|
||||
expandedKeys.value = keys;
|
||||
}
|
||||
},
|
||||
// TODO: 暂时这个版本先不实现
|
||||
filterNode(value: any, data: any) {
|
||||
if (!value) {
|
||||
return true;
|
||||
} else {
|
||||
if (matchCase) {
|
||||
// 严格匹配大写
|
||||
return data?.name?.indexOf(value) !== -1;
|
||||
} else {
|
||||
return data?.name?.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||||
}
|
||||
}
|
||||
},
|
||||
onMenu(event: MouseEvent, data: TreeData) {
|
||||
const menus: IUiMenuItem[] = [];
|
||||
menus.push({
|
||||
name: "update hierarchy",
|
||||
enabled: true,
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
updateTree();
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "fresh auto",
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
timer.create(true);
|
||||
},
|
||||
});
|
||||
menus.push({
|
||||
name: "fresh manual",
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
timer.clean();
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "fps show",
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
bridge.send(Msg.VisibleFPS, true);
|
||||
},
|
||||
});
|
||||
|
||||
menus.push({
|
||||
name: "fps hide",
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
bridge.send(Msg.VisibleFPS, false);
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "game info",
|
||||
callback(item) {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
ccui.dialog.showDialog({
|
||||
comp: GameInfo,
|
||||
title: "Game Info",
|
||||
});
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "tree icon",
|
||||
selected: config.value.showTreeIcon,
|
||||
callback(item) {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
config.value.showTreeIcon = !config.value.showTreeIcon;
|
||||
appStore().save();
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "hover inspect",
|
||||
selected: config.value.hoverInspect,
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
config.value.hoverInspect = !config.value.hoverInspect;
|
||||
appStore().save();
|
||||
},
|
||||
});
|
||||
menus.push({
|
||||
name: "click inspect",
|
||||
selected: config.value.clickInspect,
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
config.value.clickInspect = !config.value.clickInspect;
|
||||
appStore().save();
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "copy name",
|
||||
enabled: true,
|
||||
callback(item) {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
console.log(data.text);
|
||||
|
||||
if (!data.text) {
|
||||
return;
|
||||
}
|
||||
navigator.clipboard
|
||||
.writeText(data.text)
|
||||
.then(() => {
|
||||
ccui.footbar.showTips("copy success");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
bridge.send(Msg.RequestLogCustom, data.text);
|
||||
// bridge.send(Msg.ReqWriteClipboard, data.text);
|
||||
});
|
||||
},
|
||||
});
|
||||
menus.push({
|
||||
name: "visible",
|
||||
shortKey: "space",
|
||||
enabled: true,
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
onQuickVisible();
|
||||
},
|
||||
});
|
||||
menus.push({
|
||||
name: "destroy",
|
||||
enabled: true,
|
||||
callback: (item) => {
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
bridge.send(Msg.RequestDestroy, data.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
ccui.menu.showMenuByMouseEvent(event, menus);
|
||||
},
|
||||
onChangeCase() {
|
||||
matchCase.value = !matchCase.value;
|
||||
updateFilterText(filterText);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 200px;
|
||||
width: 300px;
|
||||
.engine-version {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
font-size: 10px;
|
||||
user-select: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.matchCase {
|
||||
width: 30px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
23
src/views/devtools/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp } from "vue";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
import "../global.less";
|
||||
import App from "./index.vue";
|
||||
export default CCP.init(pluginConfig, {
|
||||
ready: function (rootElement: any, args: any) {
|
||||
if (args && args.body) {
|
||||
ccui.uiElement.setBody(args.body);
|
||||
}
|
||||
if (args && args.doc) {
|
||||
ccui.uiElement.setDoc(args.doc);
|
||||
}
|
||||
const app = createApp(App);
|
||||
app.use(ccui);
|
||||
app.use(createPinia());
|
||||
app.mount(rootElement);
|
||||
},
|
||||
});
|
||||
213
src/views/devtools/index.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div id="devtools">
|
||||
<Test v-if="false"> </Test>
|
||||
<div class="head" v-show="iframes.length > 1">
|
||||
<div class="label">inspect target:</div>
|
||||
<CCSelect v-model:value="frameID" @change="onChangeFrame" :data="getFramesData()"> </CCSelect>
|
||||
</div>
|
||||
<div v-if="isShowDebug" class="find">
|
||||
<Hierarchy></Hierarchy>
|
||||
<CCDivider></CCDivider>
|
||||
<Inspector></Inspector>
|
||||
</div>
|
||||
<Find v-if="!isShowDebug"></Find>
|
||||
<CCDialog></CCDialog>
|
||||
<CCMenu></CCMenu>
|
||||
<CCFootBar :version="version"></CCFootBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { Option } from "@xuyanfeng/cc-ui/types/cc-select/const";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { defineComponent, onMounted, onUnmounted, ref, toRaw } from "vue";
|
||||
import PluginConfig from "../../../cc-plugin.config";
|
||||
import { Msg, Page, PluginEvent, RequestUseFrameData, ResponseSupportData, ResponseUseFrameData } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
import { GA_Button } from "../../ga/type";
|
||||
import { bridge } from "./bridge";
|
||||
import { Bus, BusMsg } from "./bus";
|
||||
import { FrameDetails, NodeInfoData, TreeData } from "./data";
|
||||
import Find from "./find.vue";
|
||||
import Hierarchy from "./hierarchy.vue";
|
||||
import Inspector from "./inspector.vue";
|
||||
import { appStore } from "./store";
|
||||
import Test from "./test/test.vue";
|
||||
import { Timer } from "./timer";
|
||||
import Properties from "./ui/propertys.vue";
|
||||
import SettingsVue from "./ui/settings.vue";
|
||||
import { checkSupport } from "./util";
|
||||
const { CCTree, CCFootBar, CCMenu, CCDialog, CCInput, CCButton, CCInputNumber, CCSelect, CCButtonGroup, CCCheckBox, CCColor, CCDivider } = ccui.components;
|
||||
interface FrameInfo {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: { Find, Inspector, CCMenu, Hierarchy, Test, CCFootBar, CCDialog, CCTree, CCDivider, CCButtonGroup, Properties, SettingsVue, CCInput, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor },
|
||||
name: "devtools",
|
||||
props: {},
|
||||
setup(props, ctx) {
|
||||
ga.openView(Page.Devtools);
|
||||
appStore().init();
|
||||
const isShowDebug = ref<boolean>(false);
|
||||
const iframes = ref<Array<FrameInfo>>([]);
|
||||
const { config, frameID } = storeToRefs(appStore());
|
||||
const timer = new Timer();
|
||||
timer.onWork = () => {
|
||||
checkSupport();
|
||||
};
|
||||
timer.name = "devtools";
|
||||
onMounted(() => {
|
||||
ccui.footbar.showTipsArray({
|
||||
tips: [
|
||||
"Press space in the hierarchy to quickly control the display and hiding of nodes", //
|
||||
"If you encounter any problems during use, please feel free to contact me",
|
||||
],
|
||||
});
|
||||
ccui.footbar.registerCmd({
|
||||
icon: "github",
|
||||
cb: () => {
|
||||
window.open("https://github.com/tidys/cc-inspector-chrome");
|
||||
ga.clickButton(GA_Button.Github);
|
||||
},
|
||||
});
|
||||
ccui.footbar.registerCmd({
|
||||
icon: "qq",
|
||||
cb: () => {
|
||||
window.open("https://jq.qq.com/?_wv=1027&k=5SdPdy2");
|
||||
ga.clickButton(GA_Button.QQ);
|
||||
},
|
||||
});
|
||||
ccui.footbar.registerCmd({
|
||||
icon: "support",
|
||||
cb: () => {
|
||||
window.open("https://github.com/tidys/cc-inspector-chrome/issues");
|
||||
ga.clickButton(GA_Button.Issues);
|
||||
},
|
||||
});
|
||||
Bus.on(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
timer.create(true);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
Bus.off(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
timer.clean();
|
||||
});
|
||||
// 问题:没有上下文的权限,只能操作DOM
|
||||
function _executeScript(para: Object) {
|
||||
// chrome.tabs.executeScript()//v2版本使用的函数
|
||||
const tabID = chrome.devtools.inspectedWindow.tabId;
|
||||
chrome.scripting.executeScript({ files: ["js/execute.js"], target: { tabId: tabID } }, (results: chrome.scripting.InjectionResult[]) => {});
|
||||
}
|
||||
|
||||
const funcEnableSchedule = (b: boolean) => {
|
||||
if (b) {
|
||||
timer.create();
|
||||
} else {
|
||||
timer.clean();
|
||||
}
|
||||
};
|
||||
bridge.on(Msg.ResponseTreeInfo, (event: PluginEvent) => {
|
||||
let data: Array<TreeData> = event.data;
|
||||
isShowDebug.value = true;
|
||||
});
|
||||
bridge.on(Msg.ResponseSupport, (event: PluginEvent) => {
|
||||
let data: ResponseSupportData = event.data;
|
||||
const isCocosGame: boolean = data.support;
|
||||
isShowDebug.value = isCocosGame;
|
||||
});
|
||||
bridge.on(Msg.ResponseUseFrame, (event: PluginEvent) => {
|
||||
const data: ResponseUseFrameData = event.data;
|
||||
frameID.value = data.id;
|
||||
});
|
||||
bridge.on(Msg.ResponseNodeInfo, (event: PluginEvent) => {
|
||||
let eventData: NodeInfoData = event.data;
|
||||
isShowDebug.value = true;
|
||||
});
|
||||
bridge.on(Msg.ResponseError, (event: PluginEvent) => {
|
||||
const err: string = event.data;
|
||||
ccui.footbar.showError(err);
|
||||
});
|
||||
bridge.on(Msg.ResponseUpdateFrames, (event: PluginEvent) => {
|
||||
let resFrames: FrameDetails[] = event.data;
|
||||
iframes.value = resFrames.map((item) => {
|
||||
return {
|
||||
label: item.url,
|
||||
value: item.frameID,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// el-tree的渲染key
|
||||
const defaultProps = ref<{ children: string; label: string }>({
|
||||
children: "children",
|
||||
label: "name",
|
||||
});
|
||||
|
||||
function onChangeFrame() {
|
||||
const id = Number(toRaw(frameID.value));
|
||||
bridge.send(Msg.RequestUseFrame, { id } as RequestUseFrameData);
|
||||
Bus.emit(BusMsg.ChangeContent, { id } as RequestUseFrameData);
|
||||
}
|
||||
const elLeft = ref<HTMLDivElement>();
|
||||
const version = ref(PluginConfig.manifest.version);
|
||||
return {
|
||||
version,
|
||||
defaultProps,
|
||||
frameID,
|
||||
iframes,
|
||||
isShowDebug,
|
||||
|
||||
getFramesData(): Option[] {
|
||||
const frames: FrameInfo[] = toRaw(iframes.value);
|
||||
const options: Option[] = [];
|
||||
frames.forEach((frame) => {
|
||||
options.push({
|
||||
label: frame.label,
|
||||
value: frame.value,
|
||||
});
|
||||
});
|
||||
return options;
|
||||
},
|
||||
|
||||
onChangeFrame,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
#devtools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #5c5c5c;
|
||||
color: white;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
border-bottom: solid 1px grey;
|
||||
|
||||
.label {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin: 0 3px;
|
||||
margin-right: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.find {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
src/views/devtools/inspector.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="right">
|
||||
<CCDock name="Inspector">
|
||||
<template v-slot:title>
|
||||
<Refresh :type="rotateType" @click="onClickRefresh"></Refresh>
|
||||
</template>
|
||||
<div class="inspector" @contextmenu.prevent.stop="onMenu">
|
||||
<Properties v-if="treeItemData" :data="treeItemData"></Properties>
|
||||
</div>
|
||||
</CCDock>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { IUiMenuItem } from "@xuyanfeng/cc-ui/types/cc-menu/const";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Msg, PluginEvent, RequestNodeInfoData, ResponseSupportData } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
import { GA_EventName } from "../../ga/type";
|
||||
import { bridge } from "./bridge";
|
||||
import { Bus, BusMsg } from "./bus";
|
||||
import { RotateType } from "./const";
|
||||
import { NodeInfoData } from "./data";
|
||||
import Refresh from "./refresh.vue";
|
||||
import { appStore } from "./store";
|
||||
import { Timer } from "./timer";
|
||||
import Properties from "./ui/propertys.vue";
|
||||
const { CCDock } = ccui.components;
|
||||
export default defineComponent({
|
||||
components: { Properties, CCDock, Refresh },
|
||||
setup() {
|
||||
function updateNodeInfo() {
|
||||
if (selectedUUID) {
|
||||
console.log(`update node info: ${selectedUUID}`);
|
||||
bridge.send(Msg.RequestNodeInfo, { uuid: selectedUUID } as RequestNodeInfoData);
|
||||
} else {
|
||||
treeItemData.value = null;
|
||||
}
|
||||
}
|
||||
const { config } = storeToRefs(appStore());
|
||||
const timer = new Timer();
|
||||
timer.onWork = () => {
|
||||
rotateType.value = RotateType.Loop;
|
||||
config.value.refreshInspector = true;
|
||||
appStore().save();
|
||||
updateNodeInfo();
|
||||
};
|
||||
timer.onClean = () => {
|
||||
rotateType.value = RotateType.None;
|
||||
config.value.refreshInspector = false;
|
||||
appStore().save();
|
||||
};
|
||||
timer.name = "inspector";
|
||||
const treeItemData = ref<NodeInfoData | null>(null);
|
||||
const funcEnableSchedule = (b: boolean) => {
|
||||
if (b) {
|
||||
timer.create();
|
||||
} else {
|
||||
timer.clean();
|
||||
}
|
||||
};
|
||||
let selectedUUID: string | null = null;
|
||||
const funSelectNode = (uuid: string | null) => {
|
||||
selectedUUID = uuid;
|
||||
updateNodeInfo();
|
||||
};
|
||||
function changeContent() {
|
||||
selectedUUID = null;
|
||||
treeItemData.value = null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Bus.on(BusMsg.ChangeContent, changeContent);
|
||||
Bus.on(BusMsg.SelectNode, funSelectNode);
|
||||
Bus.on(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
if (config.value.refreshInspector) {
|
||||
timer.create(true);
|
||||
} else {
|
||||
updateNodeInfo();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
Bus.off(BusMsg.ChangeContent, changeContent);
|
||||
Bus.off(BusMsg.SelectNode, funSelectNode);
|
||||
Bus.off(BusMsg.EnableSchedule, funcEnableSchedule);
|
||||
timer.clean();
|
||||
});
|
||||
bridge.on(Msg.ResponseSupport, (event: PluginEvent) => {
|
||||
let data: ResponseSupportData = event.data;
|
||||
const isCocosGame: boolean = data.support;
|
||||
if (isCocosGame) {
|
||||
} else {
|
||||
treeItemData.value = null;
|
||||
}
|
||||
});
|
||||
let simpleProperties = true;
|
||||
bridge.on(Msg.ResponseNodeInfo, (event: PluginEvent) => {
|
||||
try {
|
||||
// 因为要用到class的一些属性,传递过来的是纯数据,所以需要重新序列化一下
|
||||
let eventData: NodeInfoData = event.data;
|
||||
const nodeInfo = new NodeInfoData(eventData.uuid, eventData.group).parse(eventData, simpleProperties);
|
||||
treeItemData.value = nodeInfo;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ccui.footbar.showError(error, { title: "parse property error" });
|
||||
}
|
||||
});
|
||||
const rotateType = ref<RotateType>(RotateType.None);
|
||||
if (config.value.refreshInspector) {
|
||||
rotateType.value = RotateType.Loop;
|
||||
}
|
||||
return {
|
||||
rotateType,
|
||||
treeItemData,
|
||||
onClickRefresh() {
|
||||
updateNodeInfo();
|
||||
},
|
||||
onMenu(evnet: MouseEvent) {
|
||||
const menus: IUiMenuItem[] = [];
|
||||
menus.push({
|
||||
name: "update node info",
|
||||
callback: (item) => {
|
||||
updateNodeInfo();
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: "fresh auto",
|
||||
callback: (item) => {
|
||||
timer.create(true);
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
},
|
||||
});
|
||||
menus.push({
|
||||
name: "fresh manual",
|
||||
callback: (item) => {
|
||||
timer.clean();
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
},
|
||||
});
|
||||
menus.push({ type: ccui.menu.MenuType.Separator });
|
||||
menus.push({
|
||||
name: simpleProperties ? "show more properties" : "show simple properties",
|
||||
callback: (item) => {
|
||||
simpleProperties = !simpleProperties;
|
||||
ga.fireEventWithParam(GA_EventName.MouseMenu, item.name);
|
||||
},
|
||||
});
|
||||
ccui.menu.showMenuByMouseEvent(evnet, menus);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
|
||||
.inspector {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
src/views/devtools/refresh.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="iconfont icon_refresh_one refresh" @animationend="onAnimationend" @click="onClick" :class="getClass()"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, ref, watch } from "vue";
|
||||
import { RotateType } from "./const";
|
||||
const { CCButton } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "refresh",
|
||||
components: { CCButton },
|
||||
props: {
|
||||
type: { type: String, default: RotateType.None },
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const rotateType = ref(props.type);
|
||||
watch(
|
||||
() => props.type,
|
||||
(v) => {
|
||||
rotateType.value = v;
|
||||
}
|
||||
);
|
||||
return {
|
||||
onAnimationend() {
|
||||
rotateType.value = RotateType.None;
|
||||
},
|
||||
onClick() {
|
||||
if (rotateType.value === RotateType.Loop) {
|
||||
return;
|
||||
}
|
||||
rotateType.value = RotateType.One;
|
||||
},
|
||||
getClass() {
|
||||
if (rotateType.value === RotateType.Loop) {
|
||||
return "refresh-rotate-loop";
|
||||
}
|
||||
if (rotateType.value === RotateType.One) {
|
||||
return "refresh-rotate-one";
|
||||
}
|
||||
if (rotateType.value === RotateType.None) {
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
@time-loop: 1s;
|
||||
@time-one: 0.4s;
|
||||
|
||||
.refresh {
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: rgb(250, 207, 161);
|
||||
}
|
||||
&:active {
|
||||
color: #ffaa00;
|
||||
}
|
||||
}
|
||||
.refresh-rotate-loop {
|
||||
animation: rotate @time-loop linear infinite reverse;
|
||||
}
|
||||
|
||||
.refresh-rotate-one {
|
||||
animation: rotate @time-one linear 1 reverse;
|
||||
}
|
||||
</style>
|
||||
10
src/views/devtools/register-panel.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
|
||||
export function init() {
|
||||
if (chrome && chrome.devtools) {
|
||||
// 对应的是Elements面板的边栏
|
||||
chrome.devtools.panels.elements.createSidebarPane(CCP.manifest.name, function (sidebar) {
|
||||
sidebar.setObject({ some_data: "some data to show!" });
|
||||
});
|
||||
}
|
||||
}
|
||||
84
src/views/devtools/store.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import profile from "cc-plugin/src/ccp/profile";
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, toRaw } from "vue";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
import { PanelMsg } from "./const";
|
||||
export const enum RefreshType {
|
||||
Auto = "auto",
|
||||
Manual = "manual",
|
||||
}
|
||||
export class ConfigData {
|
||||
/**
|
||||
* 刷新类型
|
||||
*/
|
||||
refreshType: string = RefreshType.Manual;
|
||||
/**
|
||||
* 刷新间隔时间,单位ms
|
||||
*/
|
||||
refreshTime: number = 500;
|
||||
/**
|
||||
* 展开测试的section
|
||||
*/
|
||||
expandTest: boolean = false;
|
||||
/**
|
||||
* 是否自动刷新inspector
|
||||
*/
|
||||
refreshInspector: boolean = true;
|
||||
/**
|
||||
* 是否自动刷新hierarchy
|
||||
*/
|
||||
refreshHirarchy: boolean = true;
|
||||
/**
|
||||
* 当鼠标滑过节点树时,游戏是否同步Inspect
|
||||
*/
|
||||
hoverInspect: boolean = true;
|
||||
/**
|
||||
* 当节点树点击时,游戏是否同步Inspect
|
||||
*/
|
||||
clickInspect: boolean = true;
|
||||
/**
|
||||
* 显示节点树的icon
|
||||
*/
|
||||
showTreeIcon: boolean = true;
|
||||
}
|
||||
|
||||
export const appStore = defineStore("app", () => {
|
||||
const config = ref<ConfigData>(new ConfigData());
|
||||
const frameID = ref<number>(0);
|
||||
const pageShow = ref<boolean>(false);
|
||||
|
||||
function readConfigFile(file: string) {
|
||||
const data = profile.load(file) as ConfigData;
|
||||
config.value.refreshType = data.refreshType || RefreshType.Manual;
|
||||
config.value.refreshTime = data.refreshTime || 500;
|
||||
config.value.expandTest = !!data.expandTest;
|
||||
config.value.refreshHirarchy = !!data.refreshHirarchy;
|
||||
config.value.refreshInspector = !!data.refreshInspector;
|
||||
config.value.hoverInspect = !!data.hoverInspect;
|
||||
config.value.clickInspect = !!data.clickInspect;
|
||||
config.value.showTreeIcon = !!data.showTreeIcon;
|
||||
}
|
||||
return {
|
||||
frameID,
|
||||
pageShow,
|
||||
config,
|
||||
init() {
|
||||
profile.init(new ConfigData(), pluginConfig);
|
||||
if (chrome.devtools) {
|
||||
window.addEventListener(PanelMsg.Show, () => {
|
||||
pageShow.value = true;
|
||||
});
|
||||
chrome.devtools.inspectedWindow.eval("window.location.href", (url: string, ex: chrome.devtools.inspectedWindow.EvaluationExceptionInfo) => {
|
||||
readConfigFile(url);
|
||||
});
|
||||
} else {
|
||||
pageShow.value = true;
|
||||
readConfigFile(`${pluginConfig.manifest.name}.json`);
|
||||
}
|
||||
},
|
||||
save() {
|
||||
const cfg = toRaw(config.value);
|
||||
profile.save(cfg);
|
||||
},
|
||||
};
|
||||
});
|
||||
29
src/views/devtools/timer.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export class Timer {
|
||||
private timer: number | null = null;
|
||||
/**
|
||||
* 执行定时器的回调
|
||||
*/
|
||||
public onWork: Function | null = null;
|
||||
/**
|
||||
* 清理定时器的回调
|
||||
*/
|
||||
public onClean: Function | null = null;
|
||||
public duration: number = 300;
|
||||
public name: string = "";
|
||||
|
||||
create(rightNow: boolean = false) {
|
||||
this.clean();
|
||||
this.timer = setInterval(this.onWork, this.duration);
|
||||
if (rightNow) {
|
||||
this.onWork && this.onWork();
|
||||
}
|
||||
}
|
||||
clean() {
|
||||
if (this.timer === null) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
this.onClean && this.onClean();
|
||||
}
|
||||
}
|
||||
91
src/views/devtools/ui/property-engine.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="property-engine" @click="onPlaceInTree">
|
||||
<i class="icon iconfont" :class="getEngineTypeIcon()"></i>
|
||||
<div class="type">{{ data.engineType }}</div>
|
||||
<div class="name">{{ data.engineName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, toRaw } from "vue";
|
||||
import { ga } from "../../../ga";
|
||||
import { GA_EventName } from "../../../ga/type";
|
||||
import { Bus, BusMsg } from "../bus";
|
||||
import { CompType, getNodeIcon } from "../comp";
|
||||
import { EngineData } from "../data";
|
||||
export default defineComponent({
|
||||
name: "property-engine",
|
||||
components: {},
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<EngineData>,
|
||||
default: () => new EngineData(),
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
return {
|
||||
onPlaceInTree() {
|
||||
ga.fireEventWithParam(GA_EventName.Inspector, BusMsg.ShowPlace);
|
||||
Bus.emit(BusMsg.ShowPlace, toRaw(props.data));
|
||||
},
|
||||
getEngineTypeIcon() {
|
||||
return getNodeIcon(props.data.engineType as CompType) || "icon_unknown";
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@my-height: 20px;
|
||||
.property-engine {
|
||||
cursor: pointer;
|
||||
height: @my-height;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: solid #409eff 1px;
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
background-color: cornflowerblue;
|
||||
height: @my-height;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 2px;
|
||||
color: white;
|
||||
&:hover {
|
||||
color: #414141;
|
||||
}
|
||||
&:active {
|
||||
color: black;
|
||||
}
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
width: @my-height;
|
||||
margin: 0 1px 0 2px;
|
||||
}
|
||||
.type {
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.name {
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
flex: 1;
|
||||
height: @my-height;
|
||||
background-color: #d4873d;
|
||||
display: flex;
|
||||
padding: 0 5px;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
src/views/devtools/ui/property-group.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="property-group">
|
||||
<CCSection :expand="!fold" :name="group.name" :expand-by-full-header="true" :auto-slot-header="true">
|
||||
<template v-slot:title>
|
||||
<div v-if="visible" @click.stop="">
|
||||
<CCCheckBox :value="visible.data" @change="onChangeVisible"> </CCCheckBox>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:header>
|
||||
<div style="flex: 1"></div>
|
||||
<i style="" @click.stop="onLog" class="print iconfont icon_print"></i>
|
||||
</template>
|
||||
<div style="padding-left: 6px">
|
||||
<UiProp v-for="(item, index) in group.data" :key="index" :name="item.name" :value="item.value"> </UiProp>
|
||||
</div>
|
||||
</CCSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { defineComponent, onMounted, onUnmounted, PropType, ref, toRaw, watch } from "vue";
|
||||
import { Msg, RequestLogData, RequestSetPropertyData } from "../../../core/types";
|
||||
import { bridge } from "../bridge";
|
||||
import { Bus, BusMsg } from "../bus";
|
||||
import { BoolData, Group, Info, Property } from "../data";
|
||||
import UiProp from "./ui-prop.vue";
|
||||
import { VisibleProp } from "../comp";
|
||||
import { ga } from "../../../ga";
|
||||
import { GA_EventName } from "../../../ga/type";
|
||||
const { CCInput, CCSection, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "property-group",
|
||||
components: { UiProp, CCSection, CCInput, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor },
|
||||
props: {
|
||||
group: {
|
||||
type: Object as PropType<Group>,
|
||||
default: () => {
|
||||
return new Group("test");
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const funcFoldAllGroup = (b: boolean) => {
|
||||
fold.value = b;
|
||||
};
|
||||
onMounted(() => {
|
||||
Bus.on(BusMsg.FoldAllGroup, funcFoldAllGroup);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
Bus.off(BusMsg.FoldAllGroup, funcFoldAllGroup);
|
||||
});
|
||||
const fold = ref(false);
|
||||
const visible = ref<Info | null>(null);
|
||||
let visibleTarget: Property = null;
|
||||
watch(
|
||||
() => props.group,
|
||||
(v) => {
|
||||
freshVisible();
|
||||
}
|
||||
);
|
||||
function freshVisible() {
|
||||
visibleTarget = props.group.data.find((el) => {
|
||||
return el.name === VisibleProp.Enabled || el.name == VisibleProp.Active;
|
||||
});
|
||||
if (visibleTarget) {
|
||||
visible.value = visibleTarget.value;
|
||||
} else {
|
||||
visible.value = null;
|
||||
}
|
||||
}
|
||||
freshVisible();
|
||||
return {
|
||||
fold,
|
||||
visible,
|
||||
onChangeVisible(b: boolean) {
|
||||
ga.fireEventWithParam(GA_EventName.Inspector, "group-visible");
|
||||
const raw: BoolData = toRaw<Info>(visibleTarget.value) as BoolData;
|
||||
raw.data = b;
|
||||
bridge.send(Msg.RequestSetProperty, raw as RequestSetPropertyData);
|
||||
},
|
||||
onLog() {
|
||||
ga.fireEventWithParam(GA_EventName.Inspector, "group-log");
|
||||
const raw = toRaw(props);
|
||||
const data = [raw.group.id];
|
||||
bridge.send(Msg.RequestLogData, data as RequestLogData);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.property-group {
|
||||
.print {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
&:active {
|
||||
color: #ffaa00;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
src/views/devtools/ui/property-image.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div v-if="data.isImage()" class="property-image">
|
||||
<div class="box" v-if="data.data">
|
||||
<img :src="data.data" alt="图片" @click="onClickImg" class="img" />
|
||||
</div>
|
||||
<div class="url" :title="data.desc">{{ data.desc }}</div>
|
||||
<i class="print iconfont icon_print" @click="onShowValueInConsole"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, toRaw } from "vue";
|
||||
import { ImageData } from "../data";
|
||||
|
||||
export default defineComponent({
|
||||
name: "property-image",
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<ImageData>,
|
||||
default: () => new ImageData().test(),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return {
|
||||
onClickImg() {
|
||||
const url = toRaw(props.data.data);
|
||||
if (url && url.startsWith("http")) {
|
||||
window.open(url);
|
||||
}
|
||||
},
|
||||
onShowValueInConsole() {
|
||||
if (Array.isArray(props.data.path)) {
|
||||
let uuid = props.data.path[0];
|
||||
let key = props.data.path[1]; // todo 暂时只支持一级key
|
||||
if (uuid && key) {
|
||||
chrome.devtools.inspectedWindow.eval(`window.CCInspector.logValue('${uuid}','${key}')`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.property-image {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #409eff;
|
||||
border-radius: 2px;
|
||||
margin-right: 2px;
|
||||
.box {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 80px;
|
||||
width: 80px;
|
||||
justify-content: center;
|
||||
.img {
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
box-sizing: border-box;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.url {
|
||||
padding: 0 5px;
|
||||
flex: 1;
|
||||
color: gray;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.print {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
color: #d2d2d2;
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
&:active {
|
||||
color: #ffaa00;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/views/devtools/ui/propertys.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="prop ccui-scrollbar">
|
||||
<PropertyGroup v-for="(group, index) in data.group" :key="index" :group="group"></PropertyGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, watch } from "vue";
|
||||
import { Bus, BusMsg } from "../bus";
|
||||
import { NodeInfoData } from "../data";
|
||||
import PropertyGroup from "../ui/property-group.vue";
|
||||
import UiProp from "./ui-prop.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { PropertyGroup, UiProp },
|
||||
props: {
|
||||
data: {
|
||||
type: Object as PropType<NodeInfoData>,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue: NodeInfoData, oldValue: NodeInfoData) => {
|
||||
// console.log(newValue);
|
||||
if (newValue.uuid !== oldValue.uuid) {
|
||||
// 切换node,全部展开属性
|
||||
Bus.emit(BusMsg.FoldAllGroup, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.prop {
|
||||
color: white;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
67
src/views/devtools/ui/settings.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<CCProp name="refresh">
|
||||
<CCSelect :value="config.refreshType" :data="refreshOptions" @change="onChangeRefreshType" style="flex: 1"> </CCSelect>
|
||||
</CCProp>
|
||||
<CCProp name="refresh time: " v-show="isRefreshAuto()">
|
||||
<CCInputNumber style="flex: 1" :min="100" :value="config.refreshTime" @change="onChangeRefreshTime"></CCInputNumber>
|
||||
<span style="margin: 0 3px">ms</span>
|
||||
</CCProp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { Option } from "@xuyanfeng/cc-ui/types/cc-select/const";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { BusMsg } from "../bus";
|
||||
import { appStore, RefreshType } from "../store";
|
||||
const { CCInput, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCProp, CCColor } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "settings",
|
||||
components: {
|
||||
CCProp,
|
||||
CCInput,
|
||||
CCButton,
|
||||
CCInputNumber,
|
||||
CCSelect,
|
||||
CCCheckBox,
|
||||
CCColor,
|
||||
},
|
||||
props: {},
|
||||
setup(props, ctx) {
|
||||
const refreshOptions = ref<Array<Option>>([
|
||||
{ label: "auto", value: RefreshType.Auto },
|
||||
{ label: "manual", value: RefreshType.Manual },
|
||||
]);
|
||||
const { config } = storeToRefs(appStore());
|
||||
return {
|
||||
config,
|
||||
refreshOptions,
|
||||
isRefreshAuto() {
|
||||
return config.value.refreshType === RefreshType.Auto;
|
||||
},
|
||||
onChangeRefreshType(type: RefreshType) {
|
||||
const store = appStore();
|
||||
store.config.refreshType = type;
|
||||
store.save();
|
||||
bus.emit(BusMsg.UpdateSettings);
|
||||
},
|
||||
onChangeRefreshTime(v: number) {
|
||||
const store = appStore();
|
||||
store.config.refreshTime = v;
|
||||
store.save();
|
||||
bus.emit(BusMsg.UpdateSettings);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.settings {
|
||||
color: white;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
</style>
|
||||
282
src/views/devtools/ui/ui-prop.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="ui-prop">
|
||||
<CCProp :name="name" :icon="icon" :head-width="headWidth" v-model:expand="expand" :arrow="value && value.isArrayOrObject()" :slide="value && value.isNumber()" :indent="indent * 10" @change-expand="onClickFold">
|
||||
<div class="prop-value" v-if="value">
|
||||
<div v-if="value.isInvalid()" class="invalid">
|
||||
{{ formatValue(value.data) }}
|
||||
</div>
|
||||
<CCInput v-if="value.isString()" v-model:value="value.data" :disabled="value.readonly" @change="onChangeValue"> </CCInput>
|
||||
<CCTextarea v-if="value.isText()" v-model:value="value.data" :disabled="value.readonly" @change="onChangeValue"> </CCTextarea>
|
||||
<CCInputNumber v-if="value.isNumber()" v-model:value="value.data" :step="getStep()" :readonly="value.readonly" :disabled="getDisabled()" @change="onChangeValue" :tip="value.tip"></CCInputNumber>
|
||||
<div v-if="value.isVec2() || value.isVec3() || value.isVec4()" class="vec">
|
||||
<UiProp v-for="(vec, index) in value.data" :icon="!!index" head-width="auto" :key="index" :arrow="false" :value="vec.value" :name="vec.name"> </UiProp>
|
||||
</div>
|
||||
<CCSelect v-if="value.isEnum()" v-model:value="value.data" :disabled="value.readonly" :data="getEnumValues(value)" @change="onChangeValue"> </CCSelect>
|
||||
<CCCheckBox v-if="value.isBool()" v-model:value="value.data" :disabled="value.readonly" @change="onChangeValue"> </CCCheckBox>
|
||||
<CCColor v-if="value.isColor()" :show-color-text="true" :disabled="value.readonly" v-model:color="value.data" @change="onChangeValue"> </CCColor>
|
||||
<PropertyImage v-if="value.isImage()" v-model:data="(value as ImageData)"></PropertyImage>
|
||||
<Engine v-if="value.isEngine()" v-model:data="(value as EngineData)"> </Engine>
|
||||
<div v-if="value.isObject() && !expand" class="objectDesc"></div>
|
||||
<div v-if="value.isArray()" class="array">Array[{{ value.data.length }}]</div>
|
||||
<div v-if="value.isObjectCircle()" class="circle-obj" @click="onLogToConsole">circle object can't display, click to log in console</div>
|
||||
</div>
|
||||
</CCProp>
|
||||
<div v-if="value && value.isArrayOrObject()">
|
||||
<div v-show="expand && subData && subData.length">
|
||||
<UiProp v-for="(arr, index) in subData" :key="index" :indent="indent + 1" :value="arr.value" :name="getName(value.isArray(), arr)"> </UiProp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import { Option } from "@xuyanfeng/cc-ui/types/cc-select/const";
|
||||
import { defineComponent, onMounted, PropType, ref, toRaw, watch } from "vue";
|
||||
import { Msg, RequestLogData, RequestSetPropertyData } from "../../../core/types";
|
||||
import { ga } from "../../../ga";
|
||||
import { GA_EventName } from "../../../ga/type";
|
||||
import { bridge } from "../bridge";
|
||||
import { ArrayData, EngineData, EnumData, ImageData, Info, NumberData, ObjectData, Property, StringData, TextData, Vec2Data, Vec3Data } from "../data";
|
||||
import Engine from "./property-engine.vue";
|
||||
import PropertyImage from "./property-image.vue";
|
||||
const { CCInput, CCTextarea, CCProp, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "ui-prop",
|
||||
components: { PropertyImage, CCProp, Engine, CCTextarea, CCInput, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor },
|
||||
props: {
|
||||
name: { type: String, default: "" },
|
||||
indent: { type: Number, default: 0 },
|
||||
icon: { type: Boolean, default: true },
|
||||
headWidth: { type: String, default: "120px" },
|
||||
arrow: { type: Boolean, default: false },
|
||||
value: {
|
||||
type: Object as PropType<Info | EngineData | EnumData | NumberData | StringData | TextData | Vec2Data | Vec3Data | ImageData>,
|
||||
default: () => new Info(),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const expand = ref(false);
|
||||
onMounted(() => {
|
||||
expand.value = false;
|
||||
freshSubData(props.value);
|
||||
});
|
||||
watch(
|
||||
() => props.value,
|
||||
(newData, oldData) => {
|
||||
freshSubData(newData);
|
||||
}
|
||||
);
|
||||
const subData = ref<Array<Property>>([]);
|
||||
function freshSubData(data: Info) {
|
||||
const rawValue = toRaw(data);
|
||||
if (!rawValue) {
|
||||
return;
|
||||
}
|
||||
if (rawValue.isArray()) {
|
||||
subData.value = (data as ArrayData).data;
|
||||
} else if (rawValue.isObject()) {
|
||||
subData.value = (data as ObjectData).data;
|
||||
}
|
||||
}
|
||||
return {
|
||||
expand,
|
||||
subData,
|
||||
formatValue(data: any) {
|
||||
if (data === null) {
|
||||
return "null";
|
||||
} else if (data === undefined) {
|
||||
return "undefined";
|
||||
} else if (data === Infinity) {
|
||||
return "Infinity";
|
||||
} else if (Number.isNaN(data)) {
|
||||
return "NaN";
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
getEnumValues(data: any): Option[] {
|
||||
const value: EnumData = data;
|
||||
const ret: Option[] = [];
|
||||
value.values.map((item) => {
|
||||
ret.push({
|
||||
label: item.name,
|
||||
value: item.value,
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
|
||||
isImageValid() {
|
||||
return !!props.value.data;
|
||||
},
|
||||
|
||||
getName(isArray: boolean, arr: Property) {
|
||||
if (!arr || !arr.value) {
|
||||
// debugger;
|
||||
return "xxx";
|
||||
}
|
||||
const type = arr.value.type;
|
||||
if (isArray) {
|
||||
return arr.name;
|
||||
} else {
|
||||
return arr.name;
|
||||
}
|
||||
},
|
||||
onClickFold(v: boolean) {
|
||||
ga.fireEventWithParam(GA_EventName.Inspector, "expand/fold prop");
|
||||
freshSubData(props.value);
|
||||
},
|
||||
onChangeValue() {
|
||||
if (!props.value.readonly) {
|
||||
const raw = toRaw(props.value);
|
||||
bridge.send(Msg.RequestSetProperty, raw as RequestSetPropertyData);
|
||||
}
|
||||
},
|
||||
onLogToConsole() {
|
||||
ga.fireEventWithParam(GA_EventName.Inspector, "log-circle-object");
|
||||
const data = toRaw(props.value.path);
|
||||
bridge.send(Msg.RequestLogData, data as RequestLogData);
|
||||
},
|
||||
getStep() {
|
||||
if (props.value instanceof NumberData) {
|
||||
return props.value.step || 1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
getDisabled() {
|
||||
if (props.value instanceof NumberData) {
|
||||
return props.value.disabled;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ui-prop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.prop-value {
|
||||
flex: 3;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
|
||||
.invalid {
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.objectDesc {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
color: #d2d2d2;
|
||||
}
|
||||
|
||||
.array {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #d2d2d2;
|
||||
font-size: 12px;
|
||||
}
|
||||
.circle-obj {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
&:active {
|
||||
color: chocolate;
|
||||
}
|
||||
}
|
||||
|
||||
.vec {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.ui-prop {
|
||||
flex: 1;
|
||||
.cc-prop {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// #ui-prop:first-child {
|
||||
// margin-left: 0;
|
||||
// }
|
||||
|
||||
// #ui-prop:last-child {
|
||||
// margin-right: 0;
|
||||
// }
|
||||
}
|
||||
|
||||
.array-object {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slot {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.normal-data {
|
||||
margin: 0;
|
||||
min-height: 30px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.key {
|
||||
flex: 1;
|
||||
float: left;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 90px;
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
font-size: 12px;
|
||||
margin: 3px;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
6
src/views/devtools/util.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Msg, RequestSupportData } from "../../core/types";
|
||||
import { bridge } from "./bridge";
|
||||
|
||||
export function checkSupport() {
|
||||
bridge.send(Msg.RequestSupport, {} as RequestSupportData);
|
||||
}
|
||||
3
src/views/global.less
Normal file
@@ -0,0 +1,3 @@
|
||||
#app {
|
||||
background-color: #666666 !important;
|
||||
}
|
||||
15
src/views/options/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { createApp } from "vue";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
import App from "./index.vue";
|
||||
|
||||
export default CCP.init(pluginConfig, {
|
||||
ready: function (rootElement: any, args: any) {
|
||||
const app = createApp(App);
|
||||
app.use(ccui);
|
||||
app.mount(rootElement);
|
||||
},
|
||||
});
|
||||
50
src/views/options/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="options">
|
||||
<ul>
|
||||
<li>
|
||||
<div class="money">
|
||||
<div class="tips">support me with WeChat Pay</div>
|
||||
<img class="png" src="./res/money.png" alt="" />
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://patreon.com/inspector_game?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink">support me with Patreon</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { Page } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
export default defineComponent({
|
||||
name: "options",
|
||||
components: {},
|
||||
setup(props, ctx) {
|
||||
ga.openView(Page.Options);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.options {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 30px;
|
||||
user-select: none;
|
||||
.png {
|
||||
width: auto;
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
src/views/options/res/money.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
16
src/views/popup/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import "@xuyanfeng/cc-ui/dist/ccui.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/iconfont.css";
|
||||
import "@xuyanfeng/cc-ui/iconfont/use.css";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { createApp } from "vue";
|
||||
import pluginConfig from "../../../cc-plugin.config";
|
||||
import "../global.less";
|
||||
import App from "./index.vue";
|
||||
export default CCP.init(pluginConfig, {
|
||||
ready: function (rootElement: any, args: any) {
|
||||
const app = createApp(App);
|
||||
app.use(ccui);
|
||||
app.mount(rootElement);
|
||||
},
|
||||
});
|
||||
96
src/views/popup/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="popup">
|
||||
<ol>
|
||||
<li class="tips">Scan me on WeChat</li>
|
||||
<li class="tips">Add me as a friend</li>
|
||||
</ol>
|
||||
<img class="png" src="./res/friend.png" alt="" />
|
||||
<div class="foot">
|
||||
<div class="space"></div>
|
||||
<div v-if="version">ver:{{ version }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import ccui from "@xuyanfeng/cc-ui";
|
||||
import CCP from "cc-plugin/src/ccp/entry-render";
|
||||
import { ChromeConst } from "cc-plugin/src/chrome/const";
|
||||
import { defineComponent, onMounted, ref } from "vue";
|
||||
import { Page } from "../../core/types";
|
||||
import { ga } from "../../ga";
|
||||
const { CCInput, CCButton, CCInputNumber, CCSelect, CCCheckBox, CCColor } = ccui.components;
|
||||
export default defineComponent({
|
||||
name: "popup",
|
||||
components: {
|
||||
CCInput,
|
||||
CCButton,
|
||||
CCInputNumber,
|
||||
CCSelect,
|
||||
CCCheckBox,
|
||||
CCColor,
|
||||
},
|
||||
setup(props, ctx) {
|
||||
ga.openView(Page.Popup);
|
||||
const title = ref(CCP.manifest.name);
|
||||
const version = ref(CCP.manifest.version);
|
||||
let longConn: chrome.runtime.Port | null = null;
|
||||
function _initLongConn() {
|
||||
if (!longConn) {
|
||||
console.log("[popup] 初始化长连接");
|
||||
if (chrome && chrome.runtime) {
|
||||
longConn = chrome.runtime.connect({ name: "popup" });
|
||||
longConn.onMessage.addListener((data: any, sender: any) => {
|
||||
_onLongConnMsg(data, sender);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function _onLongConnMsg(data: string, sender: any) {
|
||||
// console.log( title);
|
||||
}
|
||||
onMounted(() => {
|
||||
_initLongConn();
|
||||
});
|
||||
return {
|
||||
title,
|
||||
version,
|
||||
onClickOptions() {
|
||||
if (chrome && chrome.tabs) {
|
||||
chrome.tabs.create({ url: ChromeConst.html.popup });
|
||||
}
|
||||
},
|
||||
onBtnClickGitHub() {
|
||||
console.log("onBtnClickGitHub");
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.popup {
|
||||
width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
|
||||
.tips {
|
||||
color: #000000;
|
||||
}
|
||||
.foot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
|
||||
.space {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 0 3px;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
src/views/popup/res/friend.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/views/popup/res/github.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/views/popup/res/qq.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/views/popup/res/tiezi.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |