|
| 1 | +--- |
| 2 | +import type { PioConfig } from "@/types/config"; |
| 3 | +
|
| 4 | +interface Props { |
| 5 | + config: PioConfig; |
| 6 | +} |
| 7 | +
|
| 8 | +const { config } = Astro.props; |
| 9 | +
|
| 10 | +const modelPaths = config.models ?? ["/pio/models/NOIR/noir.model3.json"]; |
| 11 | +const modelConfigs = modelPaths.map((p) => ({ path: p })); |
| 12 | +
|
| 13 | +const position = config.position === "right" ? "bottom-right" : "bottom-left"; |
| 14 | +
|
| 15 | +const tipsData: Record<string, unknown> = {}; |
| 16 | +if (config.tips) { |
| 17 | + if (config.tips.welcomeMessage) |
| 18 | + tipsData.welcomeMessage = config.tips.welcomeMessage; |
| 19 | + if (config.tips.messages) tipsData.messages = config.tips.messages; |
| 20 | + if (config.tips.duration) tipsData.duration = config.tips.duration; |
| 21 | + if (config.tips.interval) tipsData.interval = tipsData.interval; |
| 22 | +} else if (config.dialog?.welcome || config.dialog?.touch) { |
| 23 | + const welcome = config.dialog.welcome; |
| 24 | + const touch = config.dialog.touch; |
| 25 | + if (welcome) |
| 26 | + tipsData.welcomeMessage = Array.isArray(welcome) ? welcome : [welcome]; |
| 27 | + if (touch) tipsData.messages = Array.isArray(touch) ? touch : [touch]; |
| 28 | +} |
| 29 | +
|
| 30 | +const widgetConfig: Record<string, unknown> = { |
| 31 | + model: modelConfigs.length === 1 ? modelConfigs[0] : modelConfigs, |
| 32 | + position, |
| 33 | + size: config.width ?? 280, |
| 34 | + transitionDuration: 1500, |
| 35 | + transitionType: "slide", |
| 36 | + _hideAbout: config.hideAboutMenu ?? true, |
| 37 | +}; |
| 38 | +
|
| 39 | +if (config.menus?.items && config.menus.items.length > 0) { |
| 40 | + widgetConfig.menus = { |
| 41 | + items: config.menus.items.map(({ icon, label, action }) => ({ |
| 42 | + icon, |
| 43 | + label, |
| 44 | + action, |
| 45 | + })), |
| 46 | + ...(config.menus.align && { align: config.menus.align }), |
| 47 | + }; |
| 48 | +} |
| 49 | +
|
| 50 | +if (Object.keys(tipsData).length > 0) { |
| 51 | + if (!Array.isArray(widgetConfig.model)) { |
| 52 | + (widgetConfig.model as Record<string, unknown>).tips = tipsData; |
| 53 | + } |
| 54 | +} |
| 55 | +
|
| 56 | +const widgetWidth = config.width ?? 280; |
| 57 | +const hiddenOnMobile = config.hiddenOnMobile ?? true; |
| 58 | +const isRight = config.position === "right"; |
| 59 | +--- |
| 60 | + |
| 61 | +<iframe |
| 62 | + id="l2d-iframe" |
| 63 | + src="/pio/live2d-host.html" |
| 64 | + allowtransparency="true" |
| 65 | + style={"position: fixed; z-index: 999; pointer-events: none; border: none; bottom: 0;" |
| 66 | + + (isRight ? " right: 0;" : " left: 0;") |
| 67 | + + " width: " + widgetWidth + "px;" |
| 68 | + + " height: 600px;" |
| 69 | + + " opacity: 0;"} |
| 70 | + data-config={JSON.stringify(widgetConfig)} |
| 71 | + data-hidden-on-mobile={hiddenOnMobile ? "true" : "false"} |
| 72 | + data-width={widgetWidth} |
| 73 | +></iframe> |
| 74 | + |
| 75 | +<script> |
| 76 | + const el = document.getElementById("l2d-iframe") as HTMLIFrameElement; |
| 77 | + if (!el) throw new Error("l2d-iframe not found"); |
| 78 | + |
| 79 | + const menuActions: Record<string, () => void> = { |
| 80 | + home: () => (window.location.href = "/"), |
| 81 | + scrollToTop: () => window.scrollTo({ top: 0, behavior: "smooth" }), |
| 82 | + }; |
| 83 | + |
| 84 | + const config = JSON.parse(el.dataset.config || "{}"); |
| 85 | + const hiddenOnMobile = el.dataset.hiddenOnMobile === "true"; |
| 86 | + let isTransitioning = false; |
| 87 | + |
| 88 | + function applyVisibility() { |
| 89 | + if (hiddenOnMobile && window.innerWidth <= 1280) { |
| 90 | + el.style.display = "none"; |
| 91 | + } else { |
| 92 | + el.style.display = ""; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + function sendInit() { |
| 97 | + if (isTransitioning) return; |
| 98 | + if (hiddenOnMobile && window.innerWidth <= 1280) return; |
| 99 | + el.contentWindow?.postMessage({ type: "l2d-init", config }, "*"); |
| 100 | + } |
| 101 | + |
| 102 | + function handleMessage(e: MessageEvent) { |
| 103 | + if (e.source !== el.contentWindow) return; |
| 104 | + |
| 105 | + if (e.data.type === "l2d-loaded") { |
| 106 | + const h = e.data.contentHeight || 500; |
| 107 | + el.style.height = h + "px"; |
| 108 | + el.style.opacity = "1"; |
| 109 | + el.style.pointerEvents = "auto"; |
| 110 | + } |
| 111 | + |
| 112 | + if (e.data.type === "l2d-action") { |
| 113 | + menuActions[e.data.action]?.(); |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + window.addEventListener("message", handleMessage); |
| 118 | + |
| 119 | + if (el.contentDocument?.readyState === "complete") { |
| 120 | + sendInit(); |
| 121 | + } else { |
| 122 | + el.addEventListener("load", sendInit); |
| 123 | + } |
| 124 | + |
| 125 | + function setupSwupHooks() { |
| 126 | + if (!window.swup?.hooks) return; |
| 127 | + |
| 128 | + const hooks = window.swup as unknown as { |
| 129 | + hooks: { on: (event: string, handler: (...args: unknown[]) => void) => void }; |
| 130 | + }; |
| 131 | + |
| 132 | + hooks.hooks.on("link:click", () => { |
| 133 | + isTransitioning = true; |
| 134 | + }); |
| 135 | + |
| 136 | + hooks.hooks.on("visit:end", () => { |
| 137 | + isTransitioning = false; |
| 138 | + }); |
| 139 | + } |
| 140 | + |
| 141 | + if (window.swup) { |
| 142 | + setupSwupHooks(); |
| 143 | + } else { |
| 144 | + document.addEventListener("swup:enable", setupSwupHooks); |
| 145 | + } |
| 146 | + |
| 147 | + applyVisibility(); |
| 148 | + |
| 149 | + window.addEventListener("resize", () => { |
| 150 | + applyVisibility(); |
| 151 | + }); |
| 152 | +</script> |
0 commit comments