diff --git a/extension.js b/extension.js index 2c2be28..0d064c8 100644 --- a/extension.js +++ b/extension.js @@ -6,7 +6,7 @@ import { configureDiagnostics } from "./diagnostics/index.js"; export const type = "extension"; export let shuYing = new Object(); -const shuYingLocalVersion = "2.0.2.3-rc.3"; +const shuYingLocalVersion = "2.0.2.3-rc.4"; shuYing.name = "术樱包"; shuYing.editable = false; diff --git a/mengsan/ui/hand/style.css b/mengsan/ui/hand/style.css index 2dd8a45..4c783f7 100644 --- a/mengsan/ui/hand/style.css +++ b/mengsan/ui/hand/style.css @@ -5,10 +5,10 @@ } .mengsan-hand-scroll-shuying { overflow: visible !important; - left: 0 !important; + left: var(--mengsan-relic-space, 0px) !important; top: 0 !important; bottom: 0 !important; - width: 100% !important; + width: calc(100% - var(--mengsan-relic-space, 0px)) !important; height: 100% !important; padding: 0 !important; pointer-events: none; diff --git a/mengsan/ui/relic-strip.js b/mengsan/ui/relic-strip.js new file mode 100644 index 0000000..514076a --- /dev/null +++ b/mengsan/ui/relic-strip.js @@ -0,0 +1,102 @@ +export function mountRelicStrip(root, getRelics, open, createIcon, + document, window) { + const strip = document.createElement("div"); + strip.className = "mengsan-hand-relics-shuying"; + strip.setAttribute("aria-label", "持有遗物,长按查看描述"); + const before = root.style.getPropertyValue("--mengsan-relic-space"); + const priority = root.style.getPropertyPriority("--mengsan-relic-space"); + let pending = null, timer = null, signature = null, disposed = false; + const cancel = () => { + if (timer !== null) window.clearTimeout(timer); + pending?.button.classList.remove("is-pressed"); + timer = null; + pending = null; + }; + const stop = event => event.stopPropagation(); + const down = event => { + stop(event); + cancel(); + const button = event.target.closest?.("button"); + if (!button || !strip.contains(button) || event.button !== 0) return; + pending = { button, id: event.pointerId, + x: event.clientX, y: event.clientY }; + button.classList.add("is-pressed"); + timer = window.setTimeout(() => { + const relic = getRelics().find(item => item.id === button.dataset.relicId); + cancel(); + if (!disposed && relic) open(relic); + }, 500); + }; + const move = event => { + if (event.pointerId !== pending?.id) return; + if (Math.hypot(event.clientX - pending.x, + event.clientY - pending.y) > 8) cancel(); + }; + const up = event => { + if (event.pointerId === pending?.id) cancel(); + }; + const click = event => { + stop(event); + event.preventDefault(); + if (event.detail !== 0) return; + const button = event.target.closest?.("button"); + const relic = getRelics().find(item => item.id === button?.dataset.relicId); + if (relic) open(relic); + }; + const context = event => { stop(event); event.preventDefault(); }; + const listeners = [ + [strip, "pointerdown", down], [strip, "pointermove", stop], + [strip, "pointerup", stop], [strip, "click", click], + [strip, "keydown", stop], [strip, "keyup", stop], + [strip, "contextmenu", context], [strip, "scroll", cancel], + [strip, "wheel", stop], [strip, "touchstart", stop], + [strip, "touchmove", stop], [strip, "touchend", stop], + [window, "pointermove", move, true], + [window, "pointerup", up, true], + [window, "pointercancel", up, true], [window, "blur", cancel], + ]; + for (const args of listeners) args[0].addEventListener(...args.slice(1)); + root.appendChild(strip); + const refresh = () => { + if (disposed) return; + const relics = getRelics(); + const next = JSON.stringify(relics.map(item => [item.id, item.image, item.name])); + if (next === signature) return; + signature = next; + cancel(); + strip.replaceChildren(); + strip.hidden = relics.length === 0; + root.style.setProperty("--mengsan-relic-space", relics.length ? "88px" : "0px"); + for (const relic of relics) { + const button = document.createElement("button"); + button.type = "button"; + button.dataset.relicId = relic.id; + button.setAttribute("aria-label", `${relic.name},长按查看描述`); + button.setAttribute("aria-haspopup", "dialog"); + const fallback = document.createElement("span"); + fallback.textContent = relic.name; + const icon = createIcon(relic.image); + if (icon) { + fallback.hidden = true; + icon.addEventListener("error", () => { fallback.hidden = false; }, + { once: true }); + button.appendChild(icon); + } + button.appendChild(fallback); + strip.appendChild(button); + } + }; + refresh(); + return { refresh, dispose() { + if (disposed) return; + disposed = true; + cancel(); + for (const args of listeners) args[0].removeEventListener(...args.slice(1)); + strip.remove(); + const value = root.style.getPropertyValue("--mengsan-relic-space"); + if (value === "88px" || value === "0px") { + if (before) root.style.setProperty("--mengsan-relic-space", before, priority); + else root.style.removeProperty("--mengsan-relic-space"); + } + } }; +} diff --git a/mengsan/ui/relics.js b/mengsan/ui/relics.js index d5ff84c..68b865f 100644 --- a/mengsan/ui/relics.js +++ b/mengsan/ui/relics.js @@ -1,10 +1,11 @@ import { ui } from "../../../../noname.js"; import { heldRelics } from "../relics/definitions.js"; import { createRelicIcon } from "./relic-icon.js"; +import { mountRelicStrip } from "./relic-strip.js"; export function mountRelics(session, run, battleRelics) { let dialog = null, list = null, previousFocus = null, disposed = false; - const relics = heldRelics(run); + let relics = heldRelics(run), selected = null, strip = null; const button = document.createElement("button"); button.type = "button"; button.className = "mengsan-battle-piles-button-shuying " + @@ -21,6 +22,9 @@ export function mountRelics(session, run, battleRelics) { if (previousFocus?.isConnected) previousFocus.focus(); }; const refresh = () => { + relics = heldRelics(run); + button.textContent = `遗物 ${relics.length}`; + strip?.refresh(); if (!list) return; list.replaceChildren(); if (!relics.length) { @@ -28,7 +32,7 @@ export function mountRelics(session, run, battleRelics) { empty.textContent = "本次征程尚未获得遗物。"; list.appendChild(empty); } - for (const relic of relics) { + for (const relic of selected ? relics.filter(r => r.id === selected) : relics) { const row = document.createElement("section"); const icon=createRelicIcon(relic.image);if(icon)row.appendChild(icon); const name = document.createElement("h3"); @@ -41,18 +45,19 @@ export function mountRelics(session, run, battleRelics) { list.appendChild(row); } }; - button.addEventListener("click", () => { + const open = relic => { if (disposed || !session.active || dialog) return; + selected = relic?.id || null; previousFocus = document.activeElement; dialog = document.createElement("dialog"); const currentDialog = dialog; dialog.className = "mengsan-deck-dialog-shuying " + "mengsan-relic-dialog-shuying"; - dialog.setAttribute("aria-label", "遗物(道具)"); + dialog.setAttribute("aria-label", relic?.name || "遗物(道具)"); const header = document.createElement("header"); header.className = "ms-deck-header"; const title = document.createElement("h2"); - title.textContent = "遗物(道具)"; + title.textContent = relic?.name || "遗物(道具)"; const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "ms-deck-button"; @@ -73,10 +78,13 @@ export function mountRelics(session, run, battleRelics) { document.body.appendChild(dialog); try { dialog.showModal(); dismiss.focus(); } catch (error) { close(); throw error; } - }); + }; + button.addEventListener("click", () => open()); + if (ui.me) strip = mountRelicStrip(ui.me, () => heldRelics(run), + open, createRelicIcon, document, window); const dispose = () => { if (disposed) return; - disposed = true; close(); button.remove(); + disposed = true; close(); strip?.dispose(); button.remove(); }; session.ownResource(button, dispose); return { dispose, refresh }; diff --git a/mengsan/ui/style.css b/mengsan/ui/style.css index 685666d..992ae61 100644 --- a/mengsan/ui/style.css +++ b/mengsan/ui/style.css @@ -1376,6 +1376,62 @@ body.mengsan-battle-ui-shuying :is( object-fit: contain; } .mengsan-battle-relics-button-shuying { top: 162px; } +.mengsan-hand-relics-shuying { + position: absolute !important; + left: 0; + bottom: 8px; + width: 64px; + max-height: calc(100% - 16px); + padding: 8px; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + touch-action: pan-y; + pointer-events: auto; + z-index: 260; + background: #234844e6; + border-radius: 8px; + scrollbar-width: thin; + scrollbar-color: #d8ba83 #234844; +} +.mengsan-hand-relics-shuying[hidden] { display: none; } +.mengsan-hand-relics-shuying > button { + position: relative !important; + display: grid; + place-items: center; + width: 48px; + height: 48px; + flex: 0 0 48px; + padding: 0; + border: 1px solid #d8ba8380; + border-radius: 6px; + background: #17332f; + color: #fff1cb; + cursor: pointer; + touch-action: pan-y; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} +.mengsan-hand-relics-shuying > button:is(.is-pressed, :focus-visible) { + outline: 2px solid #d8ba83; + outline-offset: -2px; + background: #3b6258; +} +.mengsan-hand-relics-shuying > button > img { + width: 40px; + height: 40px; + pointer-events: none; + -webkit-user-drag: none; +} +.mengsan-hand-relics-shuying > button > span { + font: 12px/1.4 "Microsoft YaHei", sans-serif; + overflow-wrap: anywhere; +} .mengsan-relic-dialog-shuying { max-width: 620px; } .mengsan-relic-list-shuying { overflow-y: auto; padding: 12px 0; } .mengsan-relic-list-shuying section {