feat: 支持Tia看板娘模型切换
This commit is contained in:
parent
9f95504073
commit
2fc4d840e1
@ -9,9 +9,27 @@ import {
|
|||||||
DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY,
|
DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY,
|
||||||
DEFAULT_WORDPRESS_LIVE2D_SCRIPT,
|
DEFAULT_WORDPRESS_LIVE2D_SCRIPT,
|
||||||
DEFAULT_WORDPRESS_LIVE2D_SETTINGS,
|
DEFAULT_WORDPRESS_LIVE2D_SETTINGS,
|
||||||
|
DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY,
|
||||||
mountWordPressLive2DRuntime,
|
mountWordPressLive2DRuntime,
|
||||||
} from '@/components/blog/live2d/wordpressRuntimeBridge';
|
} from '@/components/blog/live2d/wordpressRuntimeBridge';
|
||||||
|
|
||||||
|
interface DeferredValue<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a promise whose resolver stays in the test so toolbar loading guards can be checked before metadata arrives.
|
||||||
|
* @returns Promise and resolver pair controlled by the active unit test.
|
||||||
|
*/
|
||||||
|
function createDeferredValue<T>(): DeferredValue<T> {
|
||||||
|
let resolve: (value: T) => void = () => undefined;
|
||||||
|
const promise = new Promise<T>((nextResolve) => {
|
||||||
|
resolve = nextResolve;
|
||||||
|
});
|
||||||
|
return { promise, resolve };
|
||||||
|
}
|
||||||
|
|
||||||
describe('mountWordPressLive2DRuntime', () => {
|
describe('mountWordPressLive2DRuntime', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
@ -48,7 +66,10 @@ describe('mountWordPressLive2DRuntime', () => {
|
|||||||
expect(document.querySelectorAll('canvas#live2d.live2d.kt-blog__live2d-canvas')).toHaveLength(1);
|
expect(document.querySelectorAll('canvas#live2d.live2d.kt-blog__live2d-canvas')).toHaveLength(1);
|
||||||
expect(initLive2D).toHaveBeenCalledTimes(1);
|
expect(initLive2D).toHaveBeenCalledTimes(1);
|
||||||
expect(window.LAppDefine).toMatchObject(DEFAULT_WORDPRESS_LIVE2D_SETTINGS);
|
expect(window.LAppDefine).toMatchObject(DEFAULT_WORDPRESS_LIVE2D_SETTINGS);
|
||||||
expect(window.LAppDefine?.MODELS).toEqual([[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY]]);
|
expect(window.LAppDefine?.MODELS).toEqual([
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY],
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('coalesces concurrent WordPress runtime script loads before calling InitLive2D', async () => {
|
it('coalesces concurrent WordPress runtime script loads before calling InitLive2D', async () => {
|
||||||
@ -80,7 +101,10 @@ describe('mountWordPressLive2DRuntime', () => {
|
|||||||
await Promise.all([firstMount, secondMount]);
|
await Promise.all([firstMount, secondMount]);
|
||||||
|
|
||||||
expect(initLive2D).toHaveBeenCalledTimes(1);
|
expect(initLive2D).toHaveBeenCalledTimes(1);
|
||||||
expect(window.LAppDefine?.MODELS).toEqual([[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY]]);
|
expect(window.LAppDefine?.MODELS).toEqual([
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY],
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps WordPress Pio custom hit areas to source motion groups', async () => {
|
it('maps WordPress Pio custom hit areas to source motion groups', async () => {
|
||||||
@ -235,10 +259,18 @@ describe('BlogLive2D', () => {
|
|||||||
expect(document.querySelector('.gptInput #live2dChatText')).not.toBeNull();
|
expect(document.querySelector('.gptInput #live2dChatText')).not.toBeNull();
|
||||||
expect(document.querySelector('.gptInput #live2dSend')).not.toBeNull();
|
expect(document.querySelector('.gptInput #live2dSend')).not.toBeNull();
|
||||||
expect(document.querySelector('.gptInput #live2dSendClose')).not.toBeNull();
|
expect(document.querySelector('.gptInput #live2dSendClose')).not.toBeNull();
|
||||||
|
expect(document.getElementById('kt-blog-pio-change')).not.toBeNull();
|
||||||
expect(document.getElementById('live2d-texture-button')).not.toBeNull();
|
expect(document.getElementById('live2d-texture-button')).not.toBeNull();
|
||||||
expect(window.LAppDefine?.canvasSize).toEqual({ width: 280, height: 250 });
|
expect(window.LAppDefine?.canvasSize).toEqual({ width: 280, height: 250 });
|
||||||
|
expect(window.LAppDefine?.IS_BIND_BUTTON).toBe(true);
|
||||||
expect(window.LAppDefine?.IS_START_TEXURE_CHANGE).toBe(true);
|
expect(window.LAppDefine?.IS_START_TEXURE_CHANGE).toBe(true);
|
||||||
|
expect(window.LAppDefine?.BUTTON_ID).toBe('kt-blog-pio-change');
|
||||||
expect(window.LAppDefine?.TEXURE_BUTTON_ID).toBe('live2d-texture-button');
|
expect(window.LAppDefine?.TEXURE_BUTTON_ID).toBe('live2d-texture-button');
|
||||||
|
expect(window.LAppDefine?.MODELS).toEqual([
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY],
|
||||||
|
[DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY],
|
||||||
|
]);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY, { credentials: 'same-origin' });
|
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY, { credentials: 'same-origin' });
|
||||||
expect(initLive2D).toHaveBeenCalledTimes(1);
|
expect(initLive2D).toHaveBeenCalledTimes(1);
|
||||||
expect(canvas?.style.transform).toBe('');
|
expect(canvas?.style.transform).toBe('');
|
||||||
@ -280,6 +312,130 @@ describe('BlogLive2D', () => {
|
|||||||
expect(document.querySelector('.waifu-tips')?.textContent).toContain('只有一套');
|
expect(document.querySelector('.waifu-tips')?.textContent).toContain('只有一套');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('switches to the Tia model through the hidden WordPress model button and lazy-loads its texture count', async () => {
|
||||||
|
vi.stubGlobal('innerWidth', 1280);
|
||||||
|
const fetchMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
json: () => Promise.resolve({ textures: ['textures/default-costume.png', 'textures/witch-costume.png'] }),
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
json: () => Promise.resolve({ textures: ['textures/default-costume.png'] }),
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
const originalAppendChild = document.body.appendChild.bind(document.body);
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
|
const element = originalAppendChild(node);
|
||||||
|
if (element instanceof HTMLScriptElement) {
|
||||||
|
window.InitLive2D = vi.fn();
|
||||||
|
window.LAppLive2DManager = function LAppLive2DManager() {};
|
||||||
|
window.LAppLive2DManager.prototype.tapEvent = vi.fn();
|
||||||
|
element.onload?.(new Event('load'));
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
mount(BlogLive2D);
|
||||||
|
await flushPromises();
|
||||||
|
const hiddenModelButton = document.getElementById('kt-blog-pio-change') as HTMLButtonElement;
|
||||||
|
const hiddenTextureButton = document.getElementById('live2d-texture-button') as HTMLButtonElement;
|
||||||
|
const hiddenModelClick = vi.fn();
|
||||||
|
const hiddenTextureClick = vi.fn();
|
||||||
|
hiddenModelButton.addEventListener('click', hiddenModelClick);
|
||||||
|
hiddenTextureButton.addEventListener('click', hiddenTextureClick);
|
||||||
|
|
||||||
|
document.querySelector<HTMLElement>('.waifu-tool .fui-user')?.click();
|
||||||
|
await flushPromises();
|
||||||
|
document.querySelector<HTMLElement>('.waifu-tool .fui-eye')?.click();
|
||||||
|
|
||||||
|
expect(hiddenModelClick).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY, { credentials: 'same-origin' });
|
||||||
|
expect(hiddenTextureClick).not.toHaveBeenCalled();
|
||||||
|
expect(document.querySelector('.waifu-tips')?.textContent).toContain('只有一套');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps texture switching disabled while the next model metadata is still loading', async () => {
|
||||||
|
vi.stubGlobal('innerWidth', 1280);
|
||||||
|
const tiaModelResponse = createDeferredValue<{
|
||||||
|
json: () => Promise<{ textures: string[] }>;
|
||||||
|
ok: boolean;
|
||||||
|
}>();
|
||||||
|
const fetchMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
json: () => Promise.resolve({ textures: ['textures/default-costume.png', 'textures/witch-costume.png'] }),
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
.mockReturnValueOnce(tiaModelResponse.promise);
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
const originalAppendChild = document.body.appendChild.bind(document.body);
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
|
const element = originalAppendChild(node);
|
||||||
|
if (element instanceof HTMLScriptElement) {
|
||||||
|
window.InitLive2D = vi.fn();
|
||||||
|
window.LAppLive2DManager = function LAppLive2DManager() {};
|
||||||
|
window.LAppLive2DManager.prototype.tapEvent = vi.fn();
|
||||||
|
element.onload?.(new Event('load'));
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
mount(BlogLive2D);
|
||||||
|
await flushPromises();
|
||||||
|
const hiddenTextureButton = document.getElementById('live2d-texture-button') as HTMLButtonElement;
|
||||||
|
const hiddenTextureClick = vi.fn();
|
||||||
|
hiddenTextureButton.addEventListener('click', hiddenTextureClick);
|
||||||
|
|
||||||
|
document.querySelector<HTMLElement>('.waifu-tool .fui-user')?.click();
|
||||||
|
document.querySelector<HTMLElement>('.waifu-tool .fui-eye')?.click();
|
||||||
|
|
||||||
|
expect(hiddenTextureClick).not.toHaveBeenCalled();
|
||||||
|
expect(document.querySelector('.waifu-tips')?.textContent).toContain('服装信息读取中');
|
||||||
|
|
||||||
|
tiaModelResponse.resolve({
|
||||||
|
json: () => Promise.resolve({ textures: ['textures/default-costume.png'] }),
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not trigger texture switching while the legacy model button is loading a model', async () => {
|
||||||
|
vi.stubGlobal('innerWidth', 1280);
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
json: () => Promise.resolve({ textures: ['textures/default-costume.png', 'textures/witch-costume.png'] }),
|
||||||
|
ok: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const originalAppendChild = document.body.appendChild.bind(document.body);
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
||||||
|
const element = originalAppendChild(node);
|
||||||
|
if (element instanceof HTMLScriptElement) {
|
||||||
|
window.InitLive2D = vi.fn();
|
||||||
|
window.LAppLive2DManager = function LAppLive2DManager() {};
|
||||||
|
window.LAppLive2DManager.prototype.tapEvent = vi.fn();
|
||||||
|
element.onload?.(new Event('load'));
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
mount(BlogLive2D);
|
||||||
|
await flushPromises();
|
||||||
|
const hiddenModelButton = document.getElementById('kt-blog-pio-change') as HTMLButtonElement;
|
||||||
|
const hiddenTextureButton = document.getElementById('live2d-texture-button') as HTMLButtonElement;
|
||||||
|
const hiddenTextureClick = vi.fn();
|
||||||
|
hiddenModelButton.disabled = true;
|
||||||
|
hiddenTextureButton.addEventListener('click', hiddenTextureClick);
|
||||||
|
|
||||||
|
document.querySelector<HTMLElement>('.waifu-tool .fui-eye')?.click();
|
||||||
|
|
||||||
|
expect(hiddenTextureClick).not.toHaveBeenCalled();
|
||||||
|
expect(document.querySelector('.waifu-tips')?.textContent).toContain('模型正在切换中');
|
||||||
|
});
|
||||||
|
|
||||||
it('starts manual costume switching from the first variant and loops back to the default texture', () => {
|
it('starts manual costume switching from the first variant and loops back to the default texture', () => {
|
||||||
const runtimeSource = readFileSync(resolve(process.cwd(), 'public/live2d/wordpress-moc/live2d.min.js'), 'utf-8');
|
const runtimeSource = readFileSync(resolve(process.cwd(), 'public/live2d/wordpress-moc/live2d.min.js'), 'utf-8');
|
||||||
|
|
||||||
|
|||||||
@ -128,6 +128,8 @@ export const DEFAULT_WORDPRESS_LIVE2D_SCRIPT = '/live2d/wordpress-moc/live2d.min
|
|||||||
|
|
||||||
export const DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY = '/api/blog/live2d/pio/moc/index.json';
|
export const DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY = '/api/blog/live2d/pio/moc/index.json';
|
||||||
|
|
||||||
|
export const DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY = '/api/blog/live2d/tia/moc/index.json';
|
||||||
|
|
||||||
export const DEFAULT_WORDPRESS_LIVE2D_SETTINGS: WordPressLive2DSettings = {
|
export const DEFAULT_WORDPRESS_LIVE2D_SETTINGS: WordPressLive2DSettings = {
|
||||||
AUDIO_ID: 'kt-blog-pio-audio',
|
AUDIO_ID: 'kt-blog-pio-audio',
|
||||||
BAN_BUTTON_CLASS: 'inactive',
|
BAN_BUTTON_CLASS: 'inactive',
|
||||||
@ -138,11 +140,11 @@ export const DEFAULT_WORDPRESS_LIVE2D_SETTINGS: WordPressLive2DSettings = {
|
|||||||
HIT_AREA_BODY: 'body',
|
HIT_AREA_BODY: 'body',
|
||||||
HIT_AREA_HEAD: 'head',
|
HIT_AREA_HEAD: 'head',
|
||||||
IS_BAN_BUTTON: true,
|
IS_BAN_BUTTON: true,
|
||||||
IS_BIND_BUTTON: false,
|
IS_BIND_BUTTON: true,
|
||||||
IS_PLAY_AUDIO: false,
|
IS_PLAY_AUDIO: false,
|
||||||
IS_SCROLL_SCALE: false,
|
IS_SCROLL_SCALE: false,
|
||||||
IS_START_TEXURE_CHANGE: true,
|
IS_START_TEXURE_CHANGE: true,
|
||||||
MODELS: [[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY]],
|
MODELS: [[DEFAULT_WORDPRESS_LIVE2D_MODEL_ENTRY], [DEFAULT_WORDPRESS_LIVE2D_TIA_MODEL_ENTRY]],
|
||||||
MOTION_GROUP_FLICK_HEAD: 'flick_head',
|
MOTION_GROUP_FLICK_HEAD: 'flick_head',
|
||||||
MOTION_GROUP_IDLE: 'idle',
|
MOTION_GROUP_IDLE: 'idle',
|
||||||
MOTION_GROUP_PINCH_IN: 'pinch_in',
|
MOTION_GROUP_PINCH_IN: 'pinch_in',
|
||||||
@ -223,9 +225,10 @@ function ensureWordPressRuntimeWidgetShell(settings: WordPressLive2DSettings): W
|
|||||||
tips.className = 'waifu-tips';
|
tips.className = 'waifu-tips';
|
||||||
const tool = createWordPressToolMenu();
|
const tool = createWordPressToolMenu();
|
||||||
const chat = createWordPressChatPanel();
|
const chat = createWordPressChatPanel();
|
||||||
|
const modelButton = createWordPressHiddenButton(settings.BUTTON_ID);
|
||||||
const textureButton = createWordPressHiddenButton(settings.TEXURE_BUTTON_ID);
|
const textureButton = createWordPressHiddenButton(settings.TEXURE_BUTTON_ID);
|
||||||
|
|
||||||
widget.replaceChildren(tips, canvas, tool, chat.container, textureButton);
|
widget.replaceChildren(tips, canvas, tool, chat.container, modelButton, textureButton);
|
||||||
if (!existingWidget) {
|
if (!existingWidget) {
|
||||||
document.body.appendChild(widget);
|
document.body.appendChild(widget);
|
||||||
}
|
}
|
||||||
@ -235,6 +238,7 @@ function ensureWordPressRuntimeWidgetShell(settings: WordPressLive2DSettings): W
|
|||||||
chat: chat.container,
|
chat: chat.container,
|
||||||
closeButton: chat.closeButton,
|
closeButton: chat.closeButton,
|
||||||
input: chat.input,
|
input: chat.input,
|
||||||
|
modelButton,
|
||||||
sendButton: chat.sendButton,
|
sendButton: chat.sendButton,
|
||||||
textureButton,
|
textureButton,
|
||||||
tips,
|
tips,
|
||||||
@ -264,9 +268,11 @@ async function initializeWordPressRuntime(
|
|||||||
mountedRuntimeCanvas = shell.canvas;
|
mountedRuntimeCanvas = shell.canvas;
|
||||||
mountedPagePointerBridge?.destroy();
|
mountedPagePointerBridge?.destroy();
|
||||||
mountedPagePointerBridge = mountWordPressPagePointerBridge(shell.canvas);
|
mountedPagePointerBridge = mountWordPressPagePointerBridge(shell.canvas);
|
||||||
const textureCount = await resolveWordPressModelTextureCount(settings);
|
const textureCounts = settings.MODELS.map(() => undefined as number | undefined);
|
||||||
|
const resolveTextureCount = createWordPressModelTextureCountResolver(settings, textureCounts);
|
||||||
|
await resolveTextureCount(0);
|
||||||
mountedWidgetController?.destroy();
|
mountedWidgetController?.destroy();
|
||||||
mountedWidgetController = mountWordPressWidgetController(shell, { textureCount });
|
mountedWidgetController = mountWordPressWidgetController(shell, { resolveTextureCount, textureCounts });
|
||||||
return createWordPressRuntimeHandle();
|
return createWordPressRuntimeHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,16 +336,47 @@ function resolveWordPressViewY(canvas: HTMLCanvasElement, deviceY: number): numb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the active WordPress MOC model JSON so the toolbar can avoid destructive single-texture reloads.
|
* Creates a per-model texture count resolver so the toolbar can avoid destructive single-texture reloads.
|
||||||
* @param settings Runtime settings whose first model entry points at the active Pio `index.json`.
|
* @param settings Runtime settings whose `MODELS` entries point at each character's `index.json`.
|
||||||
|
* @param textureCounts Mutable count cache shared with the toolbar controller.
|
||||||
|
* @returns Function that resolves and caches the texture count for a specific model index.
|
||||||
|
*/
|
||||||
|
function createWordPressModelTextureCountResolver(
|
||||||
|
settings: WordPressLive2DSettings,
|
||||||
|
textureCounts: Array<number | undefined>,
|
||||||
|
): (modelIndex: number) => Promise<number | undefined> {
|
||||||
|
const pendingCounts: Array<Promise<number | undefined> | undefined> = [];
|
||||||
|
return async function resolveWordPressTextureCountForModel(modelIndex: number): Promise<number | undefined> {
|
||||||
|
if (typeof textureCounts[modelIndex] === 'number') {
|
||||||
|
return textureCounts[modelIndex];
|
||||||
|
}
|
||||||
|
if (pendingCounts[modelIndex]) {
|
||||||
|
return pendingCounts[modelIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelEntry = settings.MODELS[modelIndex]?.[0];
|
||||||
|
if (!modelEntry || typeof window.fetch !== 'function') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingCounts[modelIndex] = resolveWordPressModelTextureCount(modelEntry)
|
||||||
|
.then((count) => {
|
||||||
|
textureCounts[modelIndex] = count;
|
||||||
|
return count;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pendingCounts[modelIndex] = undefined;
|
||||||
|
});
|
||||||
|
return pendingCounts[modelIndex];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads one WordPress MOC model JSON and counts its usable texture entries.
|
||||||
|
* @param modelEntry API URL for a character's legacy MOC `index.json`.
|
||||||
* @returns Number of usable texture entries, or `undefined` when the model JSON cannot be inspected.
|
* @returns Number of usable texture entries, or `undefined` when the model JSON cannot be inspected.
|
||||||
*/
|
*/
|
||||||
async function resolveWordPressModelTextureCount(settings: WordPressLive2DSettings): Promise<number | undefined> {
|
async function resolveWordPressModelTextureCount(modelEntry: string): Promise<number | undefined> {
|
||||||
const modelEntry = settings.MODELS[0]?.[0];
|
|
||||||
if (!modelEntry || typeof window.fetch !== 'function') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.fetch(modelEntry, { credentials: 'same-origin' });
|
const response = await window.fetch(modelEntry, { credentials: 'same-origin' });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface WordPressWidgetControllerElements {
|
|||||||
chat: HTMLElement;
|
chat: HTMLElement;
|
||||||
closeButton: HTMLButtonElement;
|
closeButton: HTMLButtonElement;
|
||||||
input: HTMLInputElement;
|
input: HTMLInputElement;
|
||||||
|
modelButton: HTMLButtonElement;
|
||||||
sendButton: HTMLButtonElement;
|
sendButton: HTMLButtonElement;
|
||||||
textureButton: HTMLButtonElement;
|
textureButton: HTMLButtonElement;
|
||||||
tips: HTMLElement;
|
tips: HTMLElement;
|
||||||
@ -25,9 +26,15 @@ export interface WordPressWidgetControllerHandle {
|
|||||||
|
|
||||||
export interface WordPressWidgetControllerOptions {
|
export interface WordPressWidgetControllerOptions {
|
||||||
/**
|
/**
|
||||||
* Number of texture entries declared by the active WordPress model JSON; single-texture models must not reload.
|
* Mutable texture counts by WordPress model index; unknown values keep destructive texture switching disabled.
|
||||||
*/
|
*/
|
||||||
textureCount?: number;
|
textureCounts?: Array<number | undefined>;
|
||||||
|
/**
|
||||||
|
* Lazily inspects the WordPress model JSON for the selected model index.
|
||||||
|
* @param modelIndex Index inside `window.LAppDefine.MODELS` that is currently selected by the legacy runtime.
|
||||||
|
* @returns Texture count for that model, or `undefined` when the metadata cannot be read.
|
||||||
|
*/
|
||||||
|
resolveTextureCount?: (modelIndex: number) => Promise<number | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,6 +49,8 @@ export function mountWordPressWidgetController(
|
|||||||
): WordPressWidgetControllerHandle {
|
): WordPressWidgetControllerHandle {
|
||||||
let hideTipsTimer = 0;
|
let hideTipsTimer = 0;
|
||||||
let closeTimer = 0;
|
let closeTimer = 0;
|
||||||
|
let activeModelIndex = 0;
|
||||||
|
const modelCount = Math.max(options.textureCounts?.length ?? 1, 1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the WordPress-style tips bubble and schedules it to fade like the original plugin.
|
* Shows the WordPress-style tips bubble and schedules it to fade like the original plugin.
|
||||||
@ -87,6 +96,19 @@ export function mountWordPressWidgetController(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the currently selected model's texture count without starting a destructive runtime action.
|
||||||
|
* @returns Texture count cached for the active model, or `undefined` while the metadata is still unknown.
|
||||||
|
*/
|
||||||
|
const getActiveTextureCount = () => options.textureCounts?.[activeModelIndex];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a background metadata read for the active model when a previous model switch made it necessary.
|
||||||
|
*/
|
||||||
|
const warmActiveTextureCount = () => {
|
||||||
|
void options.resolveTextureCount?.(activeModelIndex);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatches one toolbar action using the same feature set exposed by the WordPress plugin shell.
|
* Dispatches one toolbar action using the same feature set exposed by the WordPress plugin shell.
|
||||||
* @param action WordPress toolbar action encoded on the clicked `fui-*` icon span.
|
* @param action WordPress toolbar action encoded on the clicked `fui-*` icon span.
|
||||||
@ -111,13 +133,30 @@ export function mountWordPressWidgetController(
|
|||||||
showMessage(WORDPRESS_WAIFU_MESSAGES.about);
|
showMessage(WORDPRESS_WAIFU_MESSAGES.about);
|
||||||
break;
|
break;
|
||||||
case 'model':
|
case 'model':
|
||||||
|
if (elements.modelButton.disabled) {
|
||||||
|
showMessage(WORDPRESS_WAIFU_MESSAGES.modelLoading);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
elements.modelButton.click();
|
||||||
|
activeModelIndex = (activeModelIndex + 1) % modelCount;
|
||||||
|
warmActiveTextureCount();
|
||||||
showMessage(WORDPRESS_WAIFU_MESSAGES.model);
|
showMessage(WORDPRESS_WAIFU_MESSAGES.model);
|
||||||
break;
|
break;
|
||||||
case 'photo':
|
case 'photo':
|
||||||
saveCanvas();
|
saveCanvas();
|
||||||
break;
|
break;
|
||||||
case 'texture':
|
case 'texture':
|
||||||
if (typeof options.textureCount === 'number' && options.textureCount < 2) {
|
if (elements.modelButton.disabled) {
|
||||||
|
showMessage(WORDPRESS_WAIFU_MESSAGES.modelLoading);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const textureCount = getActiveTextureCount();
|
||||||
|
if (typeof textureCount !== 'number') {
|
||||||
|
warmActiveTextureCount();
|
||||||
|
showMessage(WORDPRESS_WAIFU_MESSAGES.textureLoading);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (textureCount < 2) {
|
||||||
showMessage(WORDPRESS_WAIFU_MESSAGES.singleTexture);
|
showMessage(WORDPRESS_WAIFU_MESSAGES.singleTexture);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,12 @@ export const WORDPRESS_WAIFU_MESSAGES = {
|
|||||||
chatOpen: '想说点什么吗?',
|
chatOpen: '想说点什么吗?',
|
||||||
close: '下次再见。',
|
close: '下次再见。',
|
||||||
copy: '复制成功,记得带上出处哦。',
|
copy: '复制成功,记得带上出处哦。',
|
||||||
model: '当前只启用 Pio 模型。',
|
model: '正在切换看板娘。',
|
||||||
|
modelLoading: '模型正在切换中,请稍后。',
|
||||||
photo: '已尝试保存当前画面。',
|
photo: '已尝试保存当前画面。',
|
||||||
singleTexture: '当前只有一套服装。',
|
singleTexture: '当前只有一套服装。',
|
||||||
texture: '正在切换 Pio 的服装。',
|
texture: '正在切换服装。',
|
||||||
|
textureLoading: '服装信息读取中。',
|
||||||
touch: ['哇,你碰到我啦。', '今天也要好好经营小店。', '有新的灵感了吗?'],
|
touch: ['哇,你碰到我啦。', '今天也要好好经营小店。', '有新的灵感了吗?'],
|
||||||
welcome: '欢迎来到 KwiTsukasa 的小站。',
|
welcome: '欢迎来到 KwiTsukasa 的小站。',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user