diff --git a/.gitignore b/.gitignore index 0ad2ba8..66bab4f 100755 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,7 @@ application-local.properties /admin-frontend/node_modules/ /workplace/ -/node_modules/ \ No newline at end of file +/node_modules/ + +### TypeScript +*.tsbuildinfo \ No newline at end of file diff --git a/packages/live2d/src/components/Live2dChatWindow.tsx b/packages/live2d/src/components/Live2dChatWindow.tsx index 26c547e..2573925 100644 --- a/packages/live2d/src/components/Live2dChatWindow.tsx +++ b/packages/live2d/src/components/Live2dChatWindow.tsx @@ -5,6 +5,7 @@ import { configContext, } from "@/live2d/context/config-context"; import { sendMessage } from "@/live2d/helpers/sendMessage"; +import { DraggableMixin } from "@/live2d/mixins/draggable"; import { consume } from "@lit/context"; import { type PropertyValues, type TemplateResult, html } from "lit"; import { property, query, state } from "lit/decorators.js"; @@ -14,6 +15,12 @@ const CHAT_PANEL_WIDTH = "min(26rem, calc(100vw - 1rem))"; const CHAT_PANEL_BOTTOM = "2rem"; const CHAT_PANEL_TRANSITION_MS = 220; +const DraggableUnoLitElement = DraggableMixin(UnoLitElement, { + storageKey: "chat-window", + targetSelector: "#live2d-chat-model", + clearTransformOnPosition: true, +}); + type PopoverCapableElement = HTMLDivElement & { hidePopover: () => void; showPopover: () => void; @@ -27,7 +34,7 @@ const isPopoverCapable = ( "function" && typeof (element as Partial).hidePopover === "function"; -export class Live2dChatWindow extends UnoLitElement { +export class Live2dChatWindow extends DraggableUnoLitElement { @consume({ context: configContext }) @property({ attribute: false }) public config?: Live2dConfig; @@ -53,6 +60,8 @@ export class Live2dChatWindow extends UnoLitElement { connectedCallback(): void { super.connectedCallback(); + // 应用保存的位置 + this.applySavedPosition(); window.addEventListener("live2d:toggle-chat-window", this.handleToggle); } @@ -63,7 +72,6 @@ export class Live2dChatWindow extends UnoLitElement { } render(): TemplateResult { - const positionStyle = `inset: auto auto ${CHAT_PANEL_BOTTOM} 50%; margin: 0; width: ${CHAT_PANEL_WIDTH}; transform: translateX(-50%); transition: opacity ${CHAT_PANEL_TRANSITION_MS}ms ease;`; const panelClasses = [ "fixed z-[10000] overflow-hidden rounded-full border border-[#eadfce] bg-[#fffaf4]/96 shadow-[0_10px_24px_rgba(15,23,42,0.08)] backdrop-blur-sm will-change-[opacity]", this._isShow @@ -80,7 +88,7 @@ export class Live2dChatWindow extends UnoLitElement { id="live2d-chat-model" popover="manual" class=${panelClasses} - style=${positionStyle} + style="inset: auto auto ${CHAT_PANEL_BOTTOM} 50%; margin: 0; width: ${CHAT_PANEL_WIDTH}; transform: translateX(-50%); transition: opacity ${CHAT_PANEL_TRANSITION_MS}ms ease;" >
- 看板娘 + + + + 看板娘
`; } @@ -71,6 +86,14 @@ export class Live2dToggle extends UnoLitElement { this.dispatchEvent(new ToggleCanvasEvent({ isShow: this._isShow })); } + handleKeydown = (e: KeyboardEvent) => { + if (e.key !== "Enter" && e.key !== " ") { + return; + } + e.preventDefault(); + this.handleClick(); + }; + handleGlobalToggle = (e: Event) => { const event = e as ToggleCanvasEvent; if (event.detail.isShow) { diff --git a/packages/live2d/src/components/Live2dWidget.tsx b/packages/live2d/src/components/Live2dWidget.tsx index f5e038b..b1c26b4 100644 --- a/packages/live2d/src/components/Live2dWidget.tsx +++ b/packages/live2d/src/components/Live2dWidget.tsx @@ -14,11 +14,16 @@ import "@/live2d/components/Live2dChatWindow"; import type { ToggleCanvasEvent } from "@/live2d/events/toggle-canvas"; import { WIDGET_DRAWER_DURATION_MS, - WIDGET_DRAWER_HIDDEN_BOTTOM, WIDGET_DRAWER_VISIBLE_BOTTOM, } from "@/live2d/helpers/widgetDrawer"; +import { DraggableMixin } from "@/live2d/mixins/draggable"; -export class Live2dWidget extends UnoLitElement { +const DraggableUnoLitElement = DraggableMixin(UnoLitElement, { + storageKey: "widget", + targetSelector: "#live2d-plugin", +}); + +export class Live2dWidget extends DraggableUnoLitElement { @consume({ context: configContext }) @property({ attribute: false }) public config?: Live2dConfig; @@ -29,7 +34,11 @@ export class Live2dWidget extends UnoLitElement { @state() private _hasMountedWidget = false; + @state() + private _isDrawerAnimating = false; + private showAnimationFrameId?: number; + private drawerAnimationTimer?: number; render(): TemplateResult { return html` @@ -49,6 +58,16 @@ export class Live2dWidget extends UnoLitElement { } } + renderLive2dTips() { + if (!this._isShow && !this._isDrawerAnimating) { + return; + } + + return html``; + } + renderLive2dWidget() { if (!this._hasMountedWidget) { return; @@ -61,24 +80,29 @@ export class Live2dWidget extends UnoLitElement { const visibilityClass = this._isShow ? "pointer-events-auto" : "pointer-events-none"; - const bottom = this._isShow - ? WIDGET_DRAWER_VISIBLE_BOTTOM - : WIDGET_DRAWER_HIDDEN_BOTTOM; + const shouldClipDrawer = !this._isShow || this._isDrawerAnimating; + const drawerBoundaryStyle = shouldClipDrawer + ? `bottom: ${WIDGET_DRAWER_VISIBLE_BOTTOM}; clip-path: inset(-100vh -100vw 0 -100vw);` + : `bottom: ${WIDGET_DRAWER_VISIBLE_BOTTOM};`; + const drawerClass = this._isShow ? "translate-y-0" : "translate-y-full"; return html`
- - - ${this.renderLive2dTools()} +
+ ${this.renderLive2dTips()} + + ${this.renderLive2dTools()} +
`; } @@ -92,6 +116,7 @@ export class Live2dWidget extends UnoLitElement { } this.cancelScheduledShow(); + this.startDrawerAnimation(); this._isShow = e.detail.isShow; this.requestUpdate(); }; @@ -108,6 +133,8 @@ export class Live2dWidget extends UnoLitElement { connectedCallback(): void { super.connectedCallback(); + // 应用保存的位置 + this.applySavedPosition(); // 页面加载时清除历史消息 // 对应原始代码中的 window.onload window.addEventListener("load", this.clearChatHistory); @@ -121,6 +148,7 @@ export class Live2dWidget extends UnoLitElement { disconnectedCallback(): void { super.disconnectedCallback(); this.cancelScheduledShow(); + this.cancelDrawerAnimation(); window.removeEventListener("load", this.clearChatHistory); window.removeEventListener( "live2d:toggle-canvas", @@ -139,6 +167,7 @@ export class Live2dWidget extends UnoLitElement { this.cancelScheduledShow(); this.showAnimationFrameId = window.requestAnimationFrame(() => { this.showAnimationFrameId = undefined; + this.startDrawerAnimation(); this._isShow = true; this.requestUpdate(); }); @@ -152,6 +181,24 @@ export class Live2dWidget extends UnoLitElement { window.cancelAnimationFrame(this.showAnimationFrameId); this.showAnimationFrameId = undefined; } + + private startDrawerAnimation(): void { + this.cancelDrawerAnimation(); + this._isDrawerAnimating = true; + this.drawerAnimationTimer = window.setTimeout(() => { + this.drawerAnimationTimer = undefined; + this._isDrawerAnimating = false; + }, WIDGET_DRAWER_DURATION_MS); + } + + private cancelDrawerAnimation(): void { + if (this.drawerAnimationTimer === undefined) { + return; + } + + window.clearTimeout(this.drawerAnimationTimer); + this.drawerAnimationTimer = undefined; + } } customElements.define("live2d-widget", Live2dWidget); diff --git a/packages/live2d/src/mixins/draggable.ts b/packages/live2d/src/mixins/draggable.ts new file mode 100644 index 0000000..d7c4c25 --- /dev/null +++ b/packages/live2d/src/mixins/draggable.ts @@ -0,0 +1,461 @@ +import { + clearPosition, + getSavedPosition, + savePosition, +} from "@/live2d/utils/drag-position"; +import type { PropertyValues } from "lit"; + +export interface DraggableOptions { + storageKey: string; + targetSelector?: string; + clearTransformOnPosition?: boolean; +} + +export interface Position { + left?: string; + right?: string; + bottom?: string; + top?: string; +} + +export interface DraggableInterface { + getSavedPosition(): Position | null; + applySavedPosition(): void; + resetPosition(): void; +} + +const DRAGGING_CLASS = "live2d-dragging"; +const DRAG_OVERLAY_ID = "live2d-drag-overlay"; +const POSITION_UNIT_PATTERN = /^-?\d+(\.\d+)?px$/; + +// TypeScript requires `any[]` for generic class mixin constructors. +// biome-ignore lint/suspicious/noExplicitAny: required by TS mixin constructor rules +type Constructor = new (...args: any[]) => T; + +type HTMLElementConstructor = Constructor; + +type HTMLElementLifecycle = HTMLElement & { + connectedCallback?(): void; + disconnectedCallback?(): void; + firstUpdated?(changedProperties: PropertyValues): void; + updated?(changedProperties: PropertyValues): void; +}; + +// Return type that includes the mixin interface +type DraggableMixinReturn = T & + Constructor; + +/** + * 为 HTMLElement 添加拖拽调整位置的功能 + * 位置信息保存在 localStorage 中 + * + * 使用方式: + * class MyElement extends DraggableMixin(LitElement, { storageKey: "my-element" }) { + * // ... + * } + */ +export const DraggableMixin = ( + SuperClass: T, + options: DraggableOptions, +): DraggableMixinReturn => { + class DraggableClass extends SuperClass implements DraggableInterface { + private _dragStartX = 0; + private _dragStartY = 0; + private _initialLeft = 0; + private _initialTop = 0; + private _isDragging = false; + private readonly _dragThreshold = 3; + private _dragElement?: HTMLElement; + private _activeDragElement?: HTMLElement; + private _overlay?: HTMLDivElement; + private _suppressNextClick = false; + + /** + * 获取保存的位置 + */ + getSavedPosition(): Position | null { + return getSavedPosition(options.storageKey); + } + + /** + * 应用保存的位置到元素 + */ + applySavedPosition(): void { + const saved = this.getSavedPosition(); + const dragElement = this._getDragElement(); + if (!saved || !dragElement) { + return; + } + const nextPosition = this._getClampedSavedPosition(saved, dragElement); + if (!nextPosition) { + clearPosition(options.storageKey); + return; + } + const style = dragElement.style; + if (nextPosition.left !== undefined) { + style.left = nextPosition.left; + style.right = "auto"; + } + if (nextPosition.right !== undefined) { + style.right = nextPosition.right; + style.left = "auto"; + } + if (nextPosition.bottom !== undefined) { + style.bottom = nextPosition.bottom; + } + if (nextPosition.top !== undefined) { + style.top = nextPosition.top; + style.bottom = "auto"; + } + if (options.clearTransformOnPosition) { + style.transform = "none"; + } + } + + /** + * 重置位置(清除保存的位置) + */ + resetPosition(): void { + clearPosition(options.storageKey); + const dragElement = this._getDragElement(); + if (!dragElement) { + return; + } + const style = dragElement.style; + style.left = ""; + style.right = ""; + style.bottom = ""; + style.top = ""; + if (options.clearTransformOnPosition) { + style.transform = ""; + } + } + + connectedCallback(): void { + const proto = this._getSuperPrototype(); + if (typeof proto.connectedCallback === "function") { + proto.connectedCallback.call(this); + } + this._setupDragListeners(); + } + + disconnectedCallback(): void { + this._removeDragListeners(); + this._removeOverlay(); + const proto = this._getSuperPrototype(); + if (typeof proto.disconnectedCallback === "function") { + proto.disconnectedCallback.call(this); + } + } + + protected firstUpdated(changedProperties: PropertyValues): void { + const proto = this._getSuperPrototype(); + if (typeof proto.firstUpdated === "function") { + proto.firstUpdated.call(this, changedProperties); + } + this._setupDragListeners(); + this.applySavedPosition(); + } + + protected updated(changedProperties: PropertyValues): void { + const proto = this._getSuperPrototype(); + if (typeof proto.updated === "function") { + proto.updated.call(this, changedProperties); + } + this._setupDragListeners(); + if (!this._isDragging) { + this.applySavedPosition(); + } + } + + private _getSuperPrototype(): HTMLElementLifecycle { + return Object.getPrototypeOf( + DraggableClass.prototype, + ) as HTMLElementLifecycle; + } + + private _getDragElement(): HTMLElement | null { + if (!options.targetSelector) { + return this; + } + return ( + this.shadowRoot?.querySelector(options.targetSelector) ?? + null + ); + } + + private _getClampedSavedPosition( + saved: Position, + dragElement: HTMLElement, + ): Position | null { + if ( + !this._isPixelPosition(saved.left) || + !this._isPixelPosition(saved.top) + ) { + return null; + } + + const rect = dragElement.getBoundingClientRect(); + const left = this._clampToViewport( + Number.parseFloat(saved.left), + rect.width, + window.innerWidth, + ); + const top = this._clampToViewport( + Number.parseFloat(saved.top), + rect.height, + window.innerHeight, + ); + + return { + left: `${left}px`, + top: `${top}px`, + }; + } + + private _isPixelPosition(value: string | undefined): value is string { + return value !== undefined && POSITION_UNIT_PATTERN.test(value); + } + + private _clampToViewport( + position: number, + elementSize: number, + viewportSize: number, + ): number { + return Math.max( + 0, + Math.min(position, Math.max(0, viewportSize - elementSize)), + ); + } + + private _setupDragListeners(): void { + const dragElement = this._getDragElement(); + if (!dragElement || dragElement === this._dragElement) { + return; + } + this._removeDragListeners(); + this._dragElement = dragElement; + dragElement.addEventListener("mousedown", this._onMouseDown); + dragElement.addEventListener("touchstart", this._onTouchStart, { + passive: false, + }); + dragElement.addEventListener("click", this._onClickCapture, true); + } + + private _removeDragListeners(): void { + this._dragElement?.removeEventListener("mousedown", this._onMouseDown); + this._dragElement?.removeEventListener("touchstart", this._onTouchStart); + this._dragElement?.removeEventListener( + "click", + this._onClickCapture, + true, + ); + this._dragElement = undefined; + this._endDrag(); + } + + private _onMouseDown = (e: MouseEvent): void => { + // 只响应左键,不响应输入框、按钮等交互元素上的拖拽 + if (e.button !== 0 || this._isInteractiveEvent(e)) { + return; + } + this._startDrag(e.clientX, e.clientY); + e.preventDefault(); + }; + + private _onTouchStart = (e: TouchEvent): void => { + if (e.touches.length !== 1 || this._isInteractiveEvent(e)) { + return; + } + const touch = e.touches[0]; + this._startDrag(touch.clientX, touch.clientY); + }; + + private _isInteractiveEvent(e: Event): boolean { + for (const target of e.composedPath()) { + if (target === this._dragElement || target === this) { + break; + } + if ( + target instanceof HTMLElement && + this._isInteractiveElement(target) + ) { + return true; + } + } + return false; + } + + private _isInteractiveElement(el: HTMLElement): boolean { + const tagName = el.tagName.toLowerCase(); + const interactiveTags = ["input", "textarea", "button", "select", "a"]; + if (interactiveTags.includes(tagName)) { + return true; + } + // 检查是否在工具栏内部 + if ( + el.closest("#live2d-tools") || + el.closest("#live2d-chat-input") || + el.closest("#live2d-chat-send") + ) { + return true; + } + return false; + } + + private _startDrag(clientX: number, clientY: number): void { + const dragElement = this._getDragElement(); + if (!dragElement) { + return; + } + const rect = dragElement.getBoundingClientRect(); + + this._dragStartX = clientX; + this._dragStartY = clientY; + this._initialLeft = rect.left; + this._initialTop = rect.top; + this._isDragging = false; + this._activeDragElement = dragElement; + + document.addEventListener("mousemove", this._onMouseMove); + document.addEventListener("mouseup", this._onMouseUp); + document.addEventListener("touchmove", this._onTouchMove, { + passive: false, + }); + document.addEventListener("touchend", this._onTouchEnd); + } + + private _onMouseMove = (e: MouseEvent): void => { + this._handleMove(e.clientX, e.clientY); + }; + + private _onTouchMove = (e: TouchEvent): void => { + if (e.touches.length !== 1) return; + e.preventDefault(); + this._handleMove(e.touches[0].clientX, e.touches[0].clientY); + }; + + private _handleMove(clientX: number, clientY: number): void { + const dragElement = this._activeDragElement ?? this._getDragElement(); + if (!dragElement) { + return; + } + const dx = clientX - this._dragStartX; + const dy = clientY - this._dragStartY; + + if (!this._isDragging) { + if ( + Math.abs(dx) > this._dragThreshold || + Math.abs(dy) > this._dragThreshold + ) { + this._isDragging = true; + this._addOverlay(); + dragElement.classList.add(DRAGGING_CLASS); + dragElement.style.cursor = "grabbing"; + } else { + return; + } + } + + const newLeft = this._initialLeft + dx; + const newTop = this._initialTop + dy; + + // 确保不拖出视口 + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const rect = dragElement.getBoundingClientRect(); + const clampedLeft = Math.max( + 0, + Math.min(newLeft, viewportWidth - rect.width), + ); + const clampedTop = Math.max( + 0, + Math.min(newTop, viewportHeight - rect.height), + ); + + dragElement.style.left = `${clampedLeft}px`; + dragElement.style.top = `${clampedTop}px`; + dragElement.style.right = "auto"; + dragElement.style.bottom = "auto"; + if (options.clearTransformOnPosition) { + dragElement.style.transform = "none"; + } + } + + private _onMouseUp = (): void => { + this._endDrag(); + }; + + private _onTouchEnd = (): void => { + this._endDrag(); + }; + + private _endDrag(): void { + const dragElement = this._activeDragElement ?? this._getDragElement(); + if (this._isDragging) { + const rect = dragElement?.getBoundingClientRect(); + if (rect && dragElement) { + // 保存位置 + savePosition(options.storageKey, { + left: `${rect.left}px`, + top: `${rect.top}px`, + }); + + dragElement.classList.remove(DRAGGING_CLASS); + dragElement.style.cursor = ""; + this._suppressNextClick = true; + window.setTimeout(() => { + this._suppressNextClick = false; + }, 0); + } + } + + document.removeEventListener("mousemove", this._onMouseMove); + document.removeEventListener("mouseup", this._onMouseUp); + document.removeEventListener("touchmove", this._onTouchMove); + document.removeEventListener("touchend", this._onTouchEnd); + + this._isDragging = false; + this._activeDragElement = undefined; + this._removeOverlay(); + } + + private _onClickCapture = (e: MouseEvent): void => { + if (!this._suppressNextClick) { + return; + } + e.preventDefault(); + e.stopImmediatePropagation(); + this._suppressNextClick = false; + }; + + /** + * 添加一个全屏 overlay 来捕获鼠标事件,避免拖拽过程中触发其他元素 + */ + private _addOverlay(): void { + if (this._overlay) return; + this._overlay = document.createElement("div"); + this._overlay.id = DRAG_OVERLAY_ID; + this._overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 99999; + cursor: grabbing; + background: transparent; + `; + document.body.appendChild(this._overlay); + } + + private _removeOverlay(): void { + if (this._overlay?.parentNode) { + this._overlay.parentNode.removeChild(this._overlay); + this._overlay = undefined; + } + } + } + + return DraggableClass as DraggableMixinReturn; +}; diff --git a/packages/live2d/src/utils/drag-position.ts b/packages/live2d/src/utils/drag-position.ts new file mode 100644 index 0000000..fc4f889 --- /dev/null +++ b/packages/live2d/src/utils/drag-position.ts @@ -0,0 +1,60 @@ +import type { Position } from "@/live2d/mixins/draggable"; + +const LIVE2D_POSITION_PREFIX = "live2d-position-"; + +/** + * 获取元素的保存位置 + */ +export const getSavedPosition = (key: string): Position | null => { + try { + const saved = localStorage.getItem(`${LIVE2D_POSITION_PREFIX}${key}`); + if (saved) { + return JSON.parse(saved) as Position; + } + } catch { + // ignore parse errors + } + return null; +}; + +/** + * 保存元素位置 + */ +export const savePosition = (key: string, position: Position): void => { + try { + localStorage.setItem(`${LIVE2D_POSITION_PREFIX}${key}`, JSON.stringify(position)); + } catch { + // ignore storage errors + } +}; + +/** + * 清除保存的位置 + */ +export const clearPosition = (key: string): void => { + try { + localStorage.removeItem(`${LIVE2D_POSITION_PREFIX}${key}`); + } catch { + // ignore storage errors + } +}; + +/** + * 清除所有 live2d 位置信息 + */ +export const clearAllPositions = (): void => { + try { + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key?.startsWith(LIVE2D_POSITION_PREFIX)) { + keysToRemove.push(key); + } + } + for (const key of keysToRemove) { + localStorage.removeItem(key); + } + } catch { + // ignore storage errors + } +}; diff --git a/packages/live2d/tsconfig.tsbuildinfo b/packages/live2d/tsconfig.tsbuildinfo deleted file mode 100644 index 49b7518..0000000 --- a/packages/live2d/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/demo.tsx","./src/env.d.ts","./src/halo-config.ts","./src/halo.ts","./src/index.ts","./src/api/chat-api.ts","./src/common/unolitelement.ts","./src/components/live2dcanvas.tsx","./src/components/live2dchatwindow.tsx","./src/components/live2dcontext.tsx","./src/components/live2dtips.tsx","./src/components/live2dtoggle.tsx","./src/components/live2dtools.tsx","./src/components/live2dwidget.tsx","./src/config/default-config.ts","./src/config/normalize-config.ts","./src/config/normalize-helpers.ts","./src/config/custom-tools/normalize-custom-tools.ts","./src/context/config-context.ts","./src/events/add-default-message.ts","./src/events/before-init.ts","./src/events/index.ts","./src/events/model-layout.ts","./src/events/model-ready.ts","./src/events/send-message.ts","./src/events/stream-message.ts","./src/events/tip-events.ts","./src/events/toggle-canvas.ts","./src/events/toggle-chat-window.ts","./src/events/types.ts","./src/helpers/createstreammessage.ts","./src/helpers/datewithinrange.ts","./src/helpers/getplugintips.ts","./src/helpers/loadfulltipsresource.ts","./src/helpers/loadmergedtips.ts","./src/helpers/loadtipsresource.ts","./src/helpers/mergetips.ts","./src/helpers/sendmessage.ts","./src/helpers/timewithinrange.ts","./src/helpers/widgetdrawer.ts","./src/helpers/widgetvisibility.ts","./src/live2d/console-status.ts","./src/live2d/model.ts","./src/live2d/runtime.ts","./src/live2d/tools/ai-chat.ts","./src/live2d/tools/asteroids.ts","./src/live2d/tools/custom-tool-config.ts","./src/live2d/tools/custom-tool.ts","./src/live2d/tools/exit.ts","./src/live2d/tools/hitokoto.ts","./src/live2d/tools/index.ts","./src/live2d/tools/info.ts","./src/live2d/tools/screenshot.ts","./src/live2d/tools/switch-model.ts","./src/live2d/tools/switch-texture.ts","./src/live2d/tools/tools.ts","./src/live2d/tools/custom-tool-actions/actions.generated.ts","./src/live2d/tools/custom-tool-actions/index.ts","./src/live2d/tools/custom-tool-actions/types.ts","./src/live2d/tools/custom-tool-actions/actions/emit-event.ts","./src/live2d/tools/custom-tool-actions/actions/load-model.ts","./src/live2d/tools/custom-tool-actions/actions/open-url.ts","./src/live2d/tools/custom-tool-actions/actions/screenshot.ts","./src/live2d/tools/custom-tool-actions/actions/send-message.ts","./src/live2d/tools/custom-tool-actions/actions/switch-model.ts","./src/live2d/tools/custom-tool-actions/actions/switch-texture.ts","./src/live2d/tools/custom-tool-actions/actions/toggle-chat.ts","./src/live2d/tools/custom-tool-actions/actions/widget-visibility.ts","./src/styles/unocss.global.css.d.ts","./src/types/assets.d.ts","./src/utils/distinctarray.ts","./src/utils/isnotempty.ts","./src/utils/isstring.ts","./src/utils/randomselection.ts","./src/utils/unomixin.ts","./src/utils/util.ts"],"version":"5.7.3"} \ No newline at end of file