diff --git a/components/Apps/WebView.tsx b/components/Apps/WebView.tsx index 0fe4008..f816a58 100644 --- a/components/Apps/WebView.tsx +++ b/components/Apps/WebView.tsx @@ -55,7 +55,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady data, }); }; - const { remountKey, processHandlers, pingHandlers, webViewRef } = useWebViewRemount({ + const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount({ pingEnabled: Platform.OS === "ios", backgroundThresholdMs: null, isFocused, @@ -83,6 +83,20 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo, onInitialLoadReady const loadEndTimeoutRef = useRef | null>(null); const focusedRef = useRef(isFocused); const initialLoadReadyNotifiedRef = useRef(false); + const previousMockApiFeatureEnabledRef = useRef(mockApiFeatureEnabled); + + useEffect(() => { + const previousEnabled = previousMockApiFeatureEnabledRef.current; + if (previousEnabled === mockApiFeatureEnabled) return; + + previousMockApiFeatureEnabledRef.current = mockApiFeatureEnabled; + addWebViewBreadcrumb("mock mode changed; remounting webview", { + previousEnabled, + enabled: mockApiFeatureEnabled, + focused: focusedRef.current, + }); + remount(); + }, [mockApiFeatureEnabled, remount]); useEffect(() => { focusedRef.current = isFocused; diff --git a/docs/generated/jrshikoku-operation-info.user.js b/docs/generated/jrshikoku-operation-info.user.js index 0b879cd..0d4dace 100644 --- a/docs/generated/jrshikoku-operation-info.user.js +++ b/docs/generated/jrshikoku-operation-info.user.js @@ -41,6 +41,46 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ }); } + function raf(callback) { + return (window.requestAnimationFrame || function(fn) { + return window.setTimeout(fn, 16); + })(callback); + } + + function cssSupports(property, value) { + try { + return !!(window.CSS && window.CSS.supports && window.CSS.supports(property, value)); + } catch (e) { + return false; + } + } + + function getSignageCompatibilityProfile() { + if (!window.__jrSignageCompat) { + var ua = navigator.userAgent || ''; + var androidMatch = ua.match(/Android\s([0-9.]+)/i); + var androidVersion = androidMatch ? parseFloat(androidMatch[1]) : 0; + var grid = cssSupports('display', 'grid'); + var aspectRatio = cssSupports('aspect-ratio', '1 / 1'); + var clipPath = cssSupports('clip-path', 'polygon(50% 0,100% 100%,0 100%)'); + var backdropFilter = cssSupports('backdrop-filter', 'blur(2px)') || cssSupports('-webkit-backdrop-filter', 'blur(2px)'); + var clampValue = cssSupports('width', 'clamp(1px,2px,3px)'); + var minValue = cssSupports('width', 'min(10px,20px)'); + var oldAndroid = !!androidMatch && androidVersion > 0 && androidVersion < 8; + window.__jrSignageCompat = { + grid: grid, + aspectRatio: aspectRatio, + clipPath: clipPath, + backdropFilter: backdropFilter, + clampValue: clampValue, + minValue: minValue, + oldAndroid: oldAndroid, + legacy: oldAndroid || !grid || !aspectRatio || !clampValue || !minValue, + }; + } + return window.__jrSignageCompat; + } + function getStatusText(status) { if (!status) return ''; var clone = status.cloneNode(true); @@ -83,6 +123,21 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ }); } + function ensureViewportMeta() { + var head = document.head || q('head'); + if (!head) return; + var meta = q('meta[name="viewport"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + head.appendChild(meta); + } + var content = 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover'; + if (meta.getAttribute('content') !== content) { + meta.setAttribute('content', content); + } + } + function applyLayoutTweaks() { setStyles('.sitettl', { display: 'none' }); setStyles('.attention', { display: 'none', width: '100vw' }); @@ -140,11 +195,17 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ style.id = 'jrs-operation-capture-style'; document.head.appendChild(style); } - style.textContent = [ - '.jrs-capture-page-wrap{display:block !important;text-align:right !important;margin:12px 0 10px !important;}', + var compat = getSignageCompatibilityProfile(); + var rules = [ + '.jrs-capture-page-wrap{display:flex !important;flex-wrap:wrap !important;justify-content:flex-end !important;gap:8px !important;margin:12px 0 10px !important;}', '.jrs-capture-page-link{display:inline-block !important;background:#0076a8 !important;color:#fff !important;padding:11px 16px !important;border-radius:999px !important;font-size:12px !important;font-weight:800 !important;line-height:1.2 !important;text-decoration:none !important;box-shadow:0 4px 12px rgba(0,118,168,.22) !important;position:relative !important;z-index:9999 !important;}', '.jrs-capture-page-link:visited{color:#fff !important;}', '.jrs-capture-page-link:active{opacity:.9 !important;transform:translateY(1px) !important;}', + '.jrs-capture-page-link.is-secondary{background:#ffffff !important;color:#0076a8 !important;border:1px solid #0099CB !important;box-shadow:none !important;}', + '.jrs-capture-page-link.is-secondary:visited{color:#0076a8 !important;}', + '.jrs-capture-page-link.is-x{background:linear-gradient(135deg,#111827 0%,#0f76a8 100%) !important;color:#fff !important;border:none !important;box-shadow:0 6px 14px rgba(15,118,168,.24) !important;}', + '.jrs-capture-page-link.is-x:visited{color:#fff !important;}', + '.jrs-capture-page-link.is-disabled,.jrs-capture-link.is-disabled,.jrs-subcapture-link.is-disabled{pointer-events:none !important;opacity:.52 !important;transform:none !important;}', '.jrs-capture-wrap{display:block !important;text-align:right !important;margin:12px 0 8px !important;}', '.jrs-capture-link{display:inline-block !important;background:#0a84ff !important;color:#fff !important;padding:10px 14px !important;border-radius:999px !important;font-size:12px !important;font-weight:700 !important;line-height:1.2 !important;text-decoration:none !important;box-shadow:0 4px 12px rgba(10,132,255,.25) !important;position:relative !important;z-index:9999 !important;}', '.jrs-capture-link:visited{color:#fff !important;}', @@ -190,6 +251,8 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ '.jrs-signage-status-alert-icon{width:clamp(18px,1.7vw,30px) !important;height:clamp(18px,1.7vw,30px) !important;object-fit:contain !important;flex:0 0 auto !important;}', '.jrs-signage-status-alert-symbol{position:relative !important;width:clamp(18px,1.7vw,30px) !important;height:clamp(16px,1.5vw,27px) !important;clip-path:polygon(50% 0,100% 100%,0 100%) !important;background:#f5a623 !important;flex:0 0 auto !important;}', '.jrs-signage-status-alert-symbol:after{content:"!" !important;position:absolute !important;left:0 !important;right:0 !important;top:28% !important;text-align:center !important;color:#fff !important;font-size:clamp(11px,1vw,18px) !important;font-weight:950 !important;line-height:1 !important;}', + '.jrs-signage-status-alert-symbol.is-fallback{width:auto !important;height:auto !important;clip-path:none !important;background:none !important;color:#d85b23 !important;font-size:18px !important;font-weight:950 !important;line-height:1 !important;}', + '.jrs-signage-status-alert-symbol.is-fallback:after{content:none !important;}', '.jrs-signage-status-alert-text{font-size:clamp(13px,1.18vw,20px) !important;font-weight:950 !important;line-height:1.2 !important;color:#d85b23 !important;}', '.jrs-signage-side-list{flex:1 1 auto !important;min-height:0 !important;padding:clamp(8px,.9vw,12px) !important;overflow:auto !important;max-height:none !important;-webkit-overflow-scrolling:touch !important;display:flex !important;flex-direction:column !important;gap:6px !important;}', '.jrs-signage-side-item{width:100% !important;display:block !important;text-align:left !important;background:#ffffff !important;color:#173443 !important;border:1px solid rgba(0,128,176,.18) !important;border-radius:8px !important;margin:0 !important;padding:clamp(8px,.9vw,12px) clamp(10px,1vw,14px) !important;font-size:clamp(13px,1.2vw,20px) !important;font-weight:900 !important;line-height:1.18 !important;cursor:pointer !important;box-shadow:none !important;}', @@ -236,7 +299,57 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ '.jrs-signage-heading h1.is-overflow{overflow:visible !important;text-overflow:clip !important;display:block !important;min-width:max-content !important;animation:jrsSignageTitleScroll 9s ease-in-out infinite alternate !important;}', '@keyframes jrsSignageTitleScroll{from{transform:translateX(0)}to{transform:translateX(calc(-1 * var(--jrs-title-overflow,0px)))}}', '@keyframes jrsSignagePulse{0%,100%{opacity:1}50%{opacity:.32}}' - ].join(''); + ]; + if (compat.legacy) { + rules = rules.concat([ + '#jrs-signage-root.is-legacy{padding:0 10px 10px 10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-shell{display:flex !important;flex-direction:row !important;gap:8px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-left{width:260px !important;flex:0 0 260px !important;gap:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-right{flex:1 1 auto !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-brand{padding:10px 14px !important;font-size:24px !important;border-radius:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-brand:before{width:10px !important;height:32px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-clock{font-size:12px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-map-card{padding:8px !important;border-radius:8px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-map-stage{height:140px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-summary{border-radius:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status{padding:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-label{font-size:11px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-line-badge{min-width:20px !important;height:20px !important;font-size:11px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-text{font-size:24px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-alert{gap:7px !important;padding:8px !important;border-radius:8px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-alert-icon{width:22px !important;height:22px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-alert-symbol{width:auto !important;height:auto !important;clip-path:none !important;background:none !important;color:#d85b23 !important;font-size:18px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-alert-symbol:after{content:none !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-status-alert-text{font-size:15px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-side-list{padding:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-side-item{padding:10px 12px !important;font-size:15px !important;border-radius:7px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-footnote{font-size:10px !important;border-radius:7px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-heading{display:flex !important;justify-content:space-between !important;padding:4px 10px !important;height:36px !important;max-height:36px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-heading h1{font-size:20px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-updated{font-size:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-tabs{padding:3px 10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-tab{min-width:28px !important;height:20px !important;font-size:11px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-info-viewport{padding:0 14px 12px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-midasi{margin:14px 0 8px !important;padding:8px 0 8px 18px !important;font-size:24px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-midasi:before{height:32px !important;min-height:32px !important;max-height:32px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-body-line{font-size:24px !important;margin-bottom:8px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty{font-size:30px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state{padding:14px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-card{width:100% !important;padding:18px 20px !important;border-radius:10px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-route{width:100% !important;max-width:520px !important;height:54px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-eyebrow{font-size:12px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-title{font-size:32px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-lead{font-size:19px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-empty-state-note{font-size:13px !important;}', + '#jrs-signage-root.is-legacy .jrs-signage-map-overlay{display:none !important;}' + ]); + } + style.textContent = rules.join(''); + } + + function postNativeMessage(payload) { + if (!window.ReactNativeWebView) return; + window.ReactNativeWebView.postMessage(JSON.stringify(payload)); } function downloadCapture(dataUrl, fileName) { @@ -269,6 +382,35 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ downloadCapture(payload.dataUrl, payload.fileName); } + function setCaptureButtonsBusy(isBusy) { + qa('.jrs-capture-page-link, .jrs-capture-link, .jrs-subcapture-link').forEach(function(element) { + if (!element || !element.classList) return; + element.classList[isBusy ? 'add' : 'remove']('is-disabled'); + element.setAttribute('aria-disabled', isBusy ? 'true' : 'false'); + }); + } + + function runCaptureAction(action) { + if (window.__jrCaptureBusy) { + return; + } + + window.__jrCaptureBusy = true; + setCaptureButtonsBusy(true); + + Promise.resolve() + .then(action) + .catch(function() { + postMessage({ error: true }); + }) + .then(function() { + window.setTimeout(function() { + window.__jrCaptureBusy = false; + setCaptureButtonsBusy(false); + }, 120); + }); + } + function parseDetailBlocks(html) { var temp = document.createElement('div'); temp.innerHTML = html; @@ -443,15 +585,622 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ currentY += updatedLines.length * 40 + 28; } - (item.blocks || []).forEach(function(block) { + var detailColumnCount = options && options.detailColumnCount ? Math.max(1, Math.min(2, options.detailColumnCount)) : 1; + var detailSections = detailColumnCount > 1 ? splitCaptureBlocksIntoSections(item.blocks || []) : []; + + if (detailColumnCount > 1 && detailSections.length > 1) { + var detailColumnGap = options && options.detailColumnGap != null ? options.detailColumnGap : 28; + var detailColumnWidth = Math.floor((boxContentWidth - detailColumnGap * (detailColumnCount - 1)) / detailColumnCount); + var detailColumnHeights = []; + var detailColumnIndex; + for (detailColumnIndex = 0; detailColumnIndex < detailColumnCount; detailColumnIndex += 1) { + detailColumnHeights.push(currentY); + } + + detailSections.forEach(function(section) { + var targetHeight = Math.min.apply(Math, detailColumnHeights); + var targetColumn = detailColumnHeights.indexOf(targetHeight); + var sectionX = boxX + boxPaddingX + targetColumn * (detailColumnWidth + detailColumnGap); + var sectionY = targetHeight; + + if (strip(section.title)) { + ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var sectionTitleLines = wrapText(ctx, section.title, detailColumnWidth - 34); + layout.push({ type: 'midasi', lines: sectionTitleLines, y: sectionY, lineHeight: 44, boxX: sectionX, boxWidth: detailColumnWidth, paddingX: 0 }); + sectionY += Math.max(72, sectionTitleLines.length * 44 + 22); + } + + (section.blocks || []).forEach(function(block) { + ctx.font = "400 31px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var sectionBodyLines = wrapText(ctx, block.text, detailColumnWidth); + layout.push({ type: 'body', lines: sectionBodyLines, y: sectionY, lineHeight: 47, boxX: sectionX, boxWidth: detailColumnWidth, paddingX: 0 }); + sectionY += sectionBodyLines.length * 47 + 14; + }); + + detailColumnHeights[targetColumn] = sectionY + 16; + }); + + currentY = Math.max.apply(Math, detailColumnHeights); + } else { + (item.blocks || []).forEach(function(block) { + if (block.type === 'badge') { + ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var badgeLines = wrapText(ctx, block.text, boxContentWidth - 34); + layout.push({ type: 'midasi', lines: badgeLines, y: currentY, lineHeight: 44, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); + currentY += Math.max(72, badgeLines.length * 44 + 22); + return; + } + + ctx.font = "400 31px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var bodyLines = wrapText(ctx, block.text, boxContentWidth); + layout.push({ type: 'body', lines: bodyLines, y: currentY, lineHeight: 47, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); + currentY += bodyLines.length * 47 + 14; + }); + } + + var boxHeight = currentY - boxY + 22; + layout.push({ type: 'detailBox', x: boxX, y: boxY, width: boxWidth, height: boxHeight }); + return boxY + boxHeight + (options.sectionGap || 34); + } + + function splitCaptureBlocksIntoSections(blocks) { + var sections = []; + var current = null; + + (blocks || []).forEach(function(block) { if (block.type === 'badge') { - ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; - var badgeLines = wrapText(ctx, block.text, boxContentWidth - 34); - layout.push({ type: 'midasi', lines: badgeLines, y: currentY, lineHeight: 44, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); - currentY += Math.max(72, badgeLines.length * 44 + 22); + current = { + title: block.text, + blocks: [] + }; + sections.push(current); return; } + if (!current) { + current = { + title: '', + blocks: [] + }; + sections.push(current); + } + + current.blocks.push(block); + }); + + return sections.filter(function(section) { + return strip(section.title) || (section.blocks && section.blocks.length); + }); + } + + + var X_CAPTURE_PAGE_WIDTH = 1080; + var X_CAPTURE_PAGE_HEIGHT = 1440; + var X_CAPTURE_SAFE_X = 80; + var X_CAPTURE_HEADER_TOP = 76; + var X_CAPTURE_CONTENT_TOP = 208; + var X_CAPTURE_CONTENT_BOTTOM = 1218; + var X_CAPTURE_FOOTER_Y = 1262; + var X_CAPTURE_ITEM_GAP = 26; + var X_CAPTURE_UNIT_GAP = 18; + + function getXCaptureNotice() { + return 'この画像は非公式アプリで生成したものです。最新情報・詳細はJR四国公式の運行情報をご確認ください。'; + } + + function getXPageNumberText(pageIndex, totalPages) { + return String(pageIndex + 1) + '/' + String(totalPages); + } + + function parseUpdatedAtValue(value) { + var text = strip(value); + if (!text) return null; + var normalized = text + .replace(/更新日時[::]?/g, '') + .replace(/年/g, '/') + .replace(/月/g, '/') + .replace(/日/g, '') + .replace(/時/g, ':') + .replace(/分/g, '') + .replace(/\./g, '/') + .replace(/-/g, '/') + .trim(); + var parsed = Date.parse(normalized); + return isNaN(parsed) ? null : parsed; + } + + function getLatestUpdatedAt(items) { + var bestText = ''; + var bestValue = null; + (items || []).forEach(function(item) { + var updatedAt = strip(item && item.updatedAt); + if (!updatedAt) return; + var parsed = parseUpdatedAtValue(updatedAt); + if (parsed == null) { + if (!bestText) bestText = updatedAt; + return; + } + if (bestValue == null || parsed > bestValue) { + bestValue = parsed; + bestText = updatedAt; + } + }); + return bestText; + } + + function buildXCoverSummary(items) { + var entries = (items || []).map(function(item) { + var title = strip(item && item.title) || '運行情報'; + var subTitle = strip(item && item.subTitle); + return subTitle ? title + ':' + subTitle : title; + }).filter(function(text) { + return !!text; + }); + + if (!entries.length) { + return '現在表示中の運行情報はありません。'; + } + + if (entries.length === 1) { + return entries[0]; + } + + if (entries.length === 2) { + return entries[0] + ' / ' + entries[1]; + } + + return entries.slice(0, 3).join(' / ') + (entries.length > 3 ? ' ほか' : ''); + } + + function measureXDetailHeader(ctx, item, contentWidth, continued) { + var textWidth = contentWidth - 54; + ctx.font = "800 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var titleLines = wrapText(ctx, strip(item.title) || '運行情報', textWidth); + ctx.font = "700 26px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var subTitleLines = strip(item.subTitle) ? wrapText(ctx, item.subTitle, textWidth) : []; + ctx.font = "500 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var updatedLines = strip(item.updatedAt) ? wrapText(ctx, item.updatedAt, textWidth) : []; + var height = 30 + titleLines.length * 44 + (subTitleLines.length ? 10 + subTitleLines.length * 32 : 0) + (updatedLines.length ? 12 + updatedLines.length * 28 : 0) + 24; + return { + titleLines: titleLines, + subTitleLines: subTitleLines, + updatedLines: updatedLines, + continued: !!continued, + height: Math.max(118, height) + }; + } + + function createXDetailUnits(ctx, item, bodyWidth) { + var units = []; + var pendingHeading = ''; + var blocks = item && item.blocks ? item.blocks : []; + + blocks.forEach(function(block) { + if (block.type === 'badge') { + pendingHeading = strip(block.text); + return; + } + + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var bodyLines = wrapText(ctx, strip(block.text), bodyWidth); + if (!bodyLines.length && !pendingHeading) return; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var headingLines = pendingHeading ? wrapText(ctx, pendingHeading, bodyWidth - 24) : []; + units.push({ + headingLines: headingLines, + bodyLines: bodyLines, + headingText: pendingHeading, + lineHeight: 39 + }); + pendingHeading = ''; + }); + + if (!units.length) { + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + units.push({ + headingLines: [], + bodyLines: wrapText(ctx, '詳細情報はJR四国公式の運行情報をご確認ください。', bodyWidth), + headingText: '', + lineHeight: 39 + }); + } + + return units; + } + + function getXUnitHeight(unit) { + var headingHeight = unit.headingLines && unit.headingLines.length ? Math.max(56, unit.headingLines.length * 35 + 20) : 0; + var bodyHeight = Math.max(unit.bodyLines.length, 1) * unit.lineHeight; + return 22 + headingHeight + (headingHeight ? 14 : 0) + bodyHeight + 18; + } + + function buildXUnitChunk(unit, availableHeight) { + var fullHeight = getXUnitHeight(unit); + if (fullHeight <= availableHeight) { + return { + chunk: { + headingLines: unit.headingLines.slice(), + bodyLines: unit.bodyLines.slice(), + lineHeight: unit.lineHeight, + height: fullHeight + }, + consumed: true, + rest: null, + height: fullHeight + }; + } + + var headingHeight = unit.headingLines && unit.headingLines.length ? Math.max(56, unit.headingLines.length * 35 + 20) : 0; + var availableForBody = availableHeight - 22 - headingHeight - (headingHeight ? 14 : 0) - 18; + var lineCapacity = Math.floor(availableForBody / unit.lineHeight); + var minimumLines = headingHeight ? 2 : 3; + if (lineCapacity < minimumLines) { + return null; + } + + var firstLines = unit.bodyLines.slice(0, lineCapacity); + if (!firstLines.length) { + return null; + } + + var restLines = unit.bodyLines.slice(lineCapacity); + var chunk = { + headingLines: unit.headingLines.slice(), + bodyLines: firstLines, + lineHeight: unit.lineHeight + }; + chunk.height = getXUnitHeight(chunk); + + return { + chunk: chunk, + consumed: restLines.length === 0, + rest: restLines.length ? { + headingLines: [], + bodyLines: restLines, + lineHeight: unit.lineHeight + } : null, + height: chunk.height + }; + } + + function createEmptyXDetailPage() { + return { items: [], usedHeight: 0 }; + } + + function paginateXDetailPages(ctx, items, contentWidth) { + var bodyWidth = contentWidth - 32; + var maxHeight = X_CAPTURE_CONTENT_BOTTOM - X_CAPTURE_CONTENT_TOP; + var pages = [createEmptyXDetailPage()]; + + for (var itemIndex = 0; itemIndex < items.length; itemIndex += 1) { + var item = items[itemIndex]; + var units = createXDetailUnits(ctx, item, bodyWidth); + var unitIndex = 0; + var continued = false; + + while (unitIndex < units.length) { + var page = pages[pages.length - 1]; + var gapBeforeItem = page.items.length ? X_CAPTURE_ITEM_GAP : 0; + var header = measureXDetailHeader(ctx, item, contentWidth, continued); + var availableForStart = maxHeight - page.usedHeight - gapBeforeItem - header.height; + var preview = buildXUnitChunk(units[unitIndex], availableForStart); + + if (!preview) { + if (page.items.length) { + pages.push(createEmptyXDetailPage()); + continue; + } + return null; + } + + var pageItem = { + header: header, + units: [] + }; + if (gapBeforeItem) { + page.usedHeight += gapBeforeItem; + } + page.items.push(pageItem); + page.usedHeight += header.height; + + while (unitIndex < units.length) { + var availableHeight = maxHeight - page.usedHeight; + var chunkInfo = buildXUnitChunk(units[unitIndex], availableHeight); + if (!chunkInfo) { + break; + } + + pageItem.units.push(chunkInfo.chunk); + page.usedHeight += chunkInfo.height; + + if (chunkInfo.consumed) { + unitIndex += 1; + } else if (chunkInfo.rest) { + units[unitIndex] = chunkInfo.rest; + } + + if (unitIndex < units.length) { + page.usedHeight += X_CAPTURE_UNIT_GAP; + } + } + + if (unitIndex < units.length) { + pages.push(createEmptyXDetailPage()); + continued = true; + } + } + } + + return pages.filter(function(page) { + return page.items.length > 0; + }); + } + + function drawXPageChrome(ctx, pageIndex, totalPages, subHeading) { + var titleX = X_CAPTURE_SAFE_X; + ctx.fillStyle = '#0099CB'; + ctx.fillRect(titleX, X_CAPTURE_HEADER_TOP + 4, 12, 76); + ctx.fillStyle = '#0f1720'; + ctx.font = "800 56px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('JR四国 列車運行情報', titleX + 28, X_CAPTURE_HEADER_TOP + 57); + ctx.fillStyle = '#4b5563'; + ctx.font = "700 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText(subHeading, titleX + 30, X_CAPTURE_HEADER_TOP + 96); + ctx.fillStyle = '#0f1720'; + ctx.font = "700 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var pageText = getXPageNumberText(pageIndex, totalPages); + var pageTextWidth = ctx.measureText(pageText).width; + ctx.fillText(pageText, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X - pageTextWidth, X_CAPTURE_HEADER_TOP + 52); + } + + function drawXFooter(ctx, pageIndex, totalPages) { + var notice = getXCaptureNotice(); + ctx.strokeStyle = '#d6dde5'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y - 18); + ctx.lineTo(X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y - 18); + ctx.stroke(); + ctx.fillStyle = '#5b6470'; + ctx.font = "400 20px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + wrapText(ctx, notice, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 90).slice(0, 3).forEach(function(line, index) { + ctx.fillText(line, X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y + index * 26); + }); + ctx.fillStyle = '#111827'; + ctx.font = "800 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var pageText = getXPageNumberText(pageIndex, totalPages); + var pageWidth = ctx.measureText(pageText).width; + ctx.fillText(pageText, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X - pageWidth, X_CAPTURE_FOOTER_Y + 52); + } + + function drawXDetailHeaderBlock(ctx, header, x, y, width) { + ctx.fillStyle = '#0099CB'; + ctx.fillRect(x, y, width, header.height); + ctx.fillStyle = '#ffffff'; + ctx.font = "800 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var textY = y + 44; + header.titleLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 44; + }); + if (header.subTitleLines.length) { + ctx.font = "700 26px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + textY += 2; + header.subTitleLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 32; + }); + } + if (header.updatedLines.length) { + ctx.font = "500 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + textY += 4; + header.updatedLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 28; + }); + } + if (header.continued) { + ctx.fillStyle = 'rgba(255,255,255,.22)'; + ctx.fillRect(x + width - 152, y + 18, 124, 34); + ctx.fillStyle = '#ffffff'; + ctx.font = "700 20px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('続き', x + width - 109, y + 41); + } + } + + function drawXUnitBlock(ctx, unit, x, y, width) { + ctx.strokeStyle = '#d7dee7'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, unit.height); + var textY = y + 34; + if (unit.headingLines.length) { + var headingHeight = Math.max(56, unit.headingLines.length * 35 + 20); + ctx.fillStyle = '#f3f8fb'; + ctx.fillRect(x, y, width, headingHeight + 20); + ctx.fillStyle = '#0099CB'; + ctx.fillRect(x + 14, y + 12, 8, headingHeight - 4); + ctx.fillStyle = '#0f1720'; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + unit.headingLines.forEach(function(line) { + ctx.fillText(line, x + 34, textY); + textY += 35; + }); + textY = y + headingHeight + 34; + } + ctx.fillStyle = '#111827'; + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + unit.bodyLines.forEach(function(line) { + ctx.fillText(line, x + 18, textY); + textY += unit.lineHeight; + }); + } + + async function buildXCoverPage(items, totalPages, pageIndex) { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + if (!ctx) return null; + + canvas.width = X_CAPTURE_PAGE_WIDTH; + canvas.height = X_CAPTURE_PAGE_HEIGHT; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + drawXPageChrome(ctx, pageIndex, totalPages, 'X投稿向け画像'); + + var summary = buildXCoverSummary(items); + ctx.font = "700 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var summaryLines = wrapText(ctx, summary, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 44).slice(0, 4); + var summaryHeight = Math.max(138, 32 + summaryLines.length * 46); + ctx.fillStyle = '#0f76a8'; + ctx.fillRect(X_CAPTURE_SAFE_X, 186, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2, summaryHeight); + ctx.fillStyle = '#ffffff'; + summaryLines.forEach(function(line, index) { + ctx.fillText(line, X_CAPTURE_SAFE_X + 24, 236 + index * 46); + }); + + var mapCanvas; + try { + mapCanvas = await buildMapImage(X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 30); + } catch (e) { + return null; + } + if (!mapCanvas) return null; + + var mapCardY = 186 + summaryHeight + 34; + var mapX = X_CAPTURE_SAFE_X + 15; + var mapY = mapCardY + 16; + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = '#c7dcea'; + ctx.lineWidth = 2; + ctx.strokeRect(X_CAPTURE_SAFE_X, mapCardY, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2, mapCanvas.height + 32); + ctx.drawImage(mapCanvas, mapX, mapY, mapCanvas.width, mapCanvas.height); + + var latestUpdatedAt = getLatestUpdatedAt(items); + var infoY = mapCardY + mapCanvas.height + 72; + ctx.fillStyle = '#0f1720'; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('現在表示中の路線図と運行情報', X_CAPTURE_SAFE_X, infoY); + if (latestUpdatedAt) { + ctx.fillStyle = '#4b5563'; + ctx.font = "500 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText(latestUpdatedAt, X_CAPTURE_SAFE_X, infoY + 40); + } + if (totalPages > 1) { + ctx.fillStyle = '#0099CB'; + ctx.font = "800 30px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('詳細は次の画像へ →', X_CAPTURE_SAFE_X, infoY + 94); + } + + drawXFooter(ctx, pageIndex, totalPages); + return canvas; + } + + function buildXDetailPage(page, pageIndex, totalPages) { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + if (!ctx) return null; + + canvas.width = X_CAPTURE_PAGE_WIDTH; + canvas.height = X_CAPTURE_PAGE_HEIGHT; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawXPageChrome(ctx, pageIndex, totalPages, '運行情報詳細'); + + var y = X_CAPTURE_CONTENT_TOP; + var width = X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2; + page.items.forEach(function(pageItem, itemIndex) { + if (itemIndex > 0) { + y += X_CAPTURE_ITEM_GAP; + } + drawXDetailHeaderBlock(ctx, pageItem.header, X_CAPTURE_SAFE_X, y, width); + y += pageItem.header.height; + pageItem.units.forEach(function(unit, unitIndex) { + drawXUnitBlock(ctx, unit, X_CAPTURE_SAFE_X, y, width); + y += unit.height; + if (unitIndex < pageItem.units.length - 1) { + y += X_CAPTURE_UNIT_GAP; + } + }); + }); + + drawXFooter(ctx, pageIndex, totalPages); + return canvas; + } + + async function renderXPostImageSet(items, fileNameBase) { + try { + if (!items || !items.length) { + postMessage({ error: true, reason: 'x-empty' }); + return; + } + + var measureCanvas = document.createElement('canvas'); + var measureCtx = measureCanvas.getContext('2d'); + if (!measureCtx) { + postMessage({ error: true }); + return; + } + + var detailPages = paginateXDetailPages(measureCtx, items, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2); + if (!detailPages || detailPages.length > 3) { + postMessage({ error: true, reason: 'x-overflow' }); + return; + } + + var totalPages = 1 + detailPages.length; + var timestamp = strip(fileNameBase) || String(Date.now()); + var cover = await buildXCoverPage(items, totalPages, 0); + if (!cover) { + postMessage({ error: true, reason: 'x-map' }); + return; + } + + var pages = [cover]; + detailPages.forEach(function(page, index) { + var detailCanvas = buildXDetailPage(page, index + 1, totalPages); + if (detailCanvas) { + pages.push(detailCanvas); + } + }); + + if (pages.length !== totalPages) { + postMessage({ error: true }); + return; + } + + var batchId = 'operation-info-x-batch-' + Date.now() + '-' + Math.floor(Math.random() * 100000); + for (var index = 0; index < pages.length; index += 1) { + postMessage({ + batchId: batchId, + batchIndex: index, + batchTotal: pages.length, + dataUrl: pages[index].toDataURL('image/png'), + fileName: index === 0 + ? 'operation-info-x-01-map-' + timestamp + '.png' + : 'operation-info-x-0' + String(index + 1) + '-detail-' + timestamp + '.png' + }); + } + } catch (e) { + postMessage({ error: true }); + } + } + + function appendCaptureSectionLayout(layout, ctx, section, startY, options) { + var boxX = options && options.x != null ? options.x : 0; + var boxY = startY; + var boxWidth = options && options.width != null ? options.width : 0; + var boxPaddingX = options && options.innerPaddingX != null ? options.innerPaddingX : 30; + var boxContentWidth = boxWidth - boxPaddingX * 2; + var currentY = boxY + 30; + + if (strip(section.title)) { + ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var badgeLines = wrapText(ctx, section.title, boxContentWidth - 34); + layout.push({ type: 'midasi', lines: badgeLines, y: currentY, lineHeight: 44, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); + currentY += Math.max(72, badgeLines.length * 44 + 22); + } + + (section.blocks || []).forEach(function(block) { ctx.font = "400 31px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; var bodyLines = wrapText(ctx, block.text, boxContentWidth); layout.push({ type: 'body', lines: bodyLines, y: currentY, lineHeight: 47, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); @@ -460,7 +1209,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ var boxHeight = currentY - boxY + 22; layout.push({ type: 'detailBox', x: boxX, y: boxY, width: boxWidth, height: boxHeight }); - return boxY + boxHeight + (options.sectionGap || 34); + return boxY + boxHeight + (options.sectionGap || 26); } function drawCaptureLayout(ctx, layout, canvasWidth, contentWidth, paddingX) { @@ -565,19 +1314,27 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ }); } - async function renderItemToImage(title, subTitle, updatedAt, detailHtml, fileNameBase) { + async function buildItemCaptureImageData(title, subTitle, updatedAt, blocks, fileNameBase) { try { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (!ctx) { - postMessage({ error: true }); - return; + return null; } - var width = 1080; + var sections = splitCaptureBlocksIntoSections(blocks); + var useSectionColumns = sections.length > 1; + + var singleWidth = 1080; var paddingX = 46; var paddingY = 38; - var contentWidth = width - paddingX * 2; + var singleContentWidth = singleWidth - paddingX * 2; + var columnGap = 24; + var maxColumns = 3; + var columnCount = useSectionColumns ? Math.max(1, Math.min(maxColumns, sections.length)) : 1; + var itemWidth = singleContentWidth; + var contentWidth = columnCount === 1 ? singleContentWidth : itemWidth * columnCount + columnGap * (columnCount - 1); + var width = contentWidth + paddingX * 2; var mapCanvas = null; try { mapCanvas = await buildMapImage(contentWidth); @@ -593,12 +1350,49 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ y += mapCanvas.height + 28; } - y = appendCaptureItemLayout(layout, ctx, { - title: title, - subTitle: subTitle, - updatedAt: updatedAt, - blocks: parseDetailBlocks(detailHtml) - }, y, { paddingX: paddingX, contentWidth: contentWidth, sectionGap: 34 }); + if (!useSectionColumns) { + y = appendCaptureItemLayout(layout, ctx, { + title: title, + subTitle: subTitle, + updatedAt: updatedAt, + blocks: blocks + }, y, { paddingX: paddingX, contentWidth: contentWidth, sectionGap: 34 }); + } else { + y = appendCaptureItemLayout(layout, ctx, { + title: title, + subTitle: subTitle, + updatedAt: '', + blocks: [] + }, y, { x: paddingX, width: contentWidth, showDetailBox: false, sectionGap: 24 }); + + if (strip(updatedAt)) { + ctx.font = "500 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var updatedLines = wrapText(ctx, updatedAt, contentWidth - 60); + layout.push({ type: 'updated', lines: updatedLines, y: y, lineHeight: 40, boxX: paddingX, boxWidth: contentWidth, paddingX: 30 }); + y += updatedLines.length * 40 + 28; + } + + var columnHeights = []; + var columnIndex; + for (columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { + columnHeights.push(y); + } + + sections.forEach(function(section) { + var targetHeight = Math.min.apply(Math, columnHeights); + var targetColumn = columnHeights.indexOf(targetHeight); + var itemX = paddingX + targetColumn * (itemWidth + columnGap); + columnHeights[targetColumn] = appendCaptureSectionLayout(layout, ctx, section, targetHeight, { + x: itemX, + width: itemWidth, + innerPaddingX: 30, + sectionGap: 26 + }); + }); + + y = Math.max.apply(Math, columnHeights); + } + layout.push({ type: 'notice', y: y, x: paddingX, width: contentWidth }); y += 54; @@ -608,10 +1402,57 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ ctx.fillRect(0, 0, canvas.width, canvas.height); drawCaptureLayout(ctx, layout, width, contentWidth, paddingX); - postMessage({ + return { dataUrl: canvas.toDataURL('image/png'), fileName: fileNameBase + '.png' - }); + }; + } catch (e) { + return null; + } + } + + async function renderItemToImage(title, subTitle, updatedAt, detailHtml, fileNameBase) { + try { + var capture = await buildItemCaptureImageData(title, subTitle, updatedAt, parseDetailBlocks(detailHtml), fileNameBase); + if (!capture) { + postMessage({ error: true }); + return; + } + postMessage(capture); + } catch (e) { + postMessage({ error: true }); + } + } + + async function renderAllItemsToSeparateImages(items, fileNameBase) { + try { + if (!items || !items.length) { + postMessage({ error: true }); + return; + } + + var batchId = 'operation-info-batch-' + Date.now() + '-' + Math.floor(Math.random() * 100000); + for (var index = 0; index < items.length; index += 1) { + var item = items[index]; + var capture = await buildItemCaptureImageData( + item.title, + item.subTitle, + item.updatedAt, + item.blocks || [], + fileNameBase + '-' + (item.infoId || (index + 1)) + ); + if (!capture) { + postMessage({ error: true }); + return; + } + postMessage({ + batchId: batchId, + batchIndex: index, + batchTotal: items.length, + dataUrl: capture.dataUrl, + fileName: capture.fileName + }); + } } catch (e) { postMessage({ error: true }); } @@ -636,7 +1477,8 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ var paddingY = 38; var singleContentWidth = singleWidth - paddingX * 2; var columnGap = 24; - var columnCount = Math.max(1, items.length); + var maxColumns = 3; + var columnCount = Math.max(1, Math.min(maxColumns, items.length)); var itemWidth = singleContentWidth; var contentWidth = columnCount === 1 ? singleContentWidth : itemWidth * columnCount + columnGap * (columnCount - 1); var width = contentWidth + paddingX * 2; @@ -664,16 +1506,62 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ } var columnHeights = []; - var index; - for (index = 0; index < columnCount; index += 1) { + var columnIndex; + for (columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { columnHeights.push(y); } - items.forEach(function(item, itemIndex) { - var columnIndex = columnCount === 1 ? 0 : itemIndex; - var itemX = paddingX + columnIndex * (itemWidth + columnGap); - var itemY = columnHeights[columnIndex]; - columnHeights[columnIndex] = appendCaptureItemLayout(layout, ctx, item, itemY, { + var wideItemIndex = -1; + if (columnCount >= 3 && items.length >= 3) { + var itemEstimates = items.map(function(item, index) { + var estimateLayout = []; + var estimatedEnd = appendCaptureItemLayout(estimateLayout, ctx, item, 0, { + x: 0, + width: itemWidth, + innerPaddingX: 30, + sectionGap: 26 + }); + return { + index: index, + height: estimatedEnd, + sectionCount: splitCaptureBlocksIntoSections(item.blocks || []).length + }; + }).sort(function(a, b) { return b.height - a.height; }); + var tallest = itemEstimates[0]; + var secondTallest = itemEstimates[1] || { height: 0 }; + var restAverage = itemEstimates.slice(1).reduce(function(total, entry) { + return total + entry.height; + }, 0) / Math.max(1, itemEstimates.length - 1); + if ( + tallest && + tallest.sectionCount >= 4 && + tallest.height > 1800 && + tallest.height > Math.max(secondTallest.height * 1.35, restAverage * 1.55) + ) { + wideItemIndex = tallest.index; + } + } + + if (wideItemIndex >= 0) { + var wideWidth = itemWidth * 2 + columnGap; + var wideEnd = appendCaptureItemLayout(layout, ctx, items[wideItemIndex], y, { + x: paddingX, + width: wideWidth, + innerPaddingX: 30, + sectionGap: 26, + detailColumnCount: 2, + detailColumnGap: 28 + }); + columnHeights[0] = wideEnd; + columnHeights[1] = wideEnd; + } + + items.forEach(function(item, index) { + if (index === wideItemIndex) return; + var targetHeight = Math.min.apply(Math, columnHeights); + var targetColumn = columnHeights.indexOf(targetHeight); + var itemX = paddingX + targetColumn * (itemWidth + columnGap); + columnHeights[targetColumn] = appendCaptureItemLayout(layout, ctx, item, targetHeight, { x: itemX, width: itemWidth, innerPaddingX: 30, @@ -742,20 +1630,22 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ link.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - var updatedAtNode = q('.upd_time', dd); - var subTitleNode = q('.delay_subttl', heading); - var sectionHtml = getSubsectionHtml(midasi); - if (!sectionHtml) { - postMessage({ error: true }); - return; - } - renderItemToImage( - getTitleText(heading), - subTitleNode ? subTitleNode.textContent : '', - updatedAtNode ? updatedAtNode.textContent : '', - sectionHtml, - 'operation-info-' + infoId + '-section-' + index + '-' + Date.now() - ); + runCaptureAction(function() { + var updatedAtNode = q('.upd_time', dd); + var subTitleNode = q('.delay_subttl', heading); + var sectionHtml = getSubsectionHtml(midasi); + if (!sectionHtml) { + postMessage({ error: true }); + return; + } + return renderItemToImage( + getTitleText(heading), + subTitleNode ? subTitleNode.textContent : '', + updatedAtNode ? updatedAtNode.textContent : '', + sectionHtml, + 'operation-info-' + infoId + '-section-' + index + '-' + Date.now() + ); + }); }; wrap.appendChild(link); if (midasi.nextSibling) { @@ -771,33 +1661,84 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ if (!infoRoot) return; var items = getOperationItems(); - var existingWrap = q('.jrs-capture-page-wrap'); + var wrap = q('.jrs-capture-page-wrap'); if (!items.length) { - if (existingWrap && existingWrap.parentNode) existingWrap.parentNode.removeChild(existingWrap); + if (wrap && wrap.parentNode) wrap.parentNode.removeChild(wrap); return; } - if (existingWrap) return; + if (!wrap) { + wrap = document.createElement('div'); + wrap.className = 'jrs-capture-page-wrap'; + if (infoRoot.parentNode) { + infoRoot.parentNode.insertBefore(wrap, infoRoot); + } + } - var wrap = document.createElement('div'); - wrap.className = 'jrs-capture-page-wrap'; - var link = document.createElement('a'); - link.href = '#'; - link.className = 'jrs-capture-page-link'; - link.textContent = '表示中の運行情報をすべて切り出す'; - link.onclick = function(event) { - event.preventDefault(); - event.stopPropagation(); - renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now()); - }; - wrap.appendChild(link); + var allLink = q('.jrs-capture-page-link.is-all', wrap); + if (!allLink) { + allLink = document.createElement('a'); + allLink.href = '#'; + allLink.className = 'jrs-capture-page-link is-all'; + allLink.textContent = '表示中の運行情報をすべて切り出す'; + allLink.onclick = function(event) { + event.preventDefault(); + event.stopPropagation(); + runCaptureAction(function() { + return renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now()); + }); + }; + wrap.appendChild(allLink); + } - if (infoRoot.parentNode) { - infoRoot.parentNode.insertBefore(wrap, infoRoot); + var batchLink = q('.jrs-capture-page-link.is-batch', wrap); + if (items.length > 1) { + if (!batchLink) { + batchLink = document.createElement('a'); + batchLink.href = '#'; + batchLink.className = 'jrs-capture-page-link is-secondary is-batch'; + batchLink.textContent = '項目ごとに複数画像で共有'; + batchLink.onclick = function(event) { + event.preventDefault(); + event.stopPropagation(); + runCaptureAction(function() { + return renderAllItemsToSeparateImages(getOperationItems(), 'operation-info-share-' + Date.now()); + }); + }; + wrap.appendChild(batchLink); + } + } else if (batchLink && batchLink.parentNode) { + batchLink.parentNode.removeChild(batchLink); + } + + var xLink = q('.jrs-capture-page-link.is-x', wrap); + if (!xLink) { + xLink = document.createElement('a'); + xLink.href = '#'; + xLink.className = 'jrs-capture-page-link is-x'; + xLink.textContent = 'X投稿向け画像を作成'; + xLink.onclick = function(event) { + event.preventDefault(); + event.stopPropagation(); + runCaptureAction(function() { + return renderXPostImageSet(getOperationItems(), String(Date.now())); + }); + }; + wrap.appendChild(xLink); } } + function removeCaptureButtons() { + removeNodes(document, '.jrs-capture-page-wrap, .jrs-capture-wrap, .jrs-subcapture-wrap'); + } + function installCaptureButtons() { + var nativeLayout = window.__jrsNativeOperationLayout || {}; + if (!nativeLayout.enableCapture || isSignageViewport()) { + removeCaptureButtons(); + return; + } + installPageCaptureButton(); qa('.delay_info dd[dinfo_id]').forEach(function(dd) { var infoId = dd.getAttribute('dinfo_id'); @@ -822,19 +1763,21 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ link.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - var updatedAtNode = q('.upd_time', dd); - var subTitleNode = q('.delay_subttl', heading); - if (!detailNode.innerHTML) { - postMessage({ error: true }); - return; - } - renderItemToImage( - getTitleText(heading), - subTitleNode ? subTitleNode.textContent : '', - updatedAtNode ? updatedAtNode.textContent : '', - detailNode.innerHTML, - 'operation-info-' + infoId + '-' + Date.now() - ); + runCaptureAction(function() { + var updatedAtNode = q('.upd_time', dd); + var subTitleNode = q('.delay_subttl', heading); + if (!detailNode.innerHTML) { + postMessage({ error: true }); + return; + } + return renderItemToImage( + getTitleText(heading), + subTitleNode ? subTitleNode.textContent : '', + updatedAtNode ? updatedAtNode.textContent : '', + detailNode.innerHTML, + 'operation-info-' + infoId + '-' + Date.now() + ); + }); }; wrap.appendChild(link); dd.insertBefore(wrap, dd.firstChild); @@ -846,6 +1789,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ function isSignageViewport() { var nativeLayout = window.__jrsNativeOperationLayout || {}; + if (!nativeLayout.enableLandscapeMode) return false; if (nativeLayout.forceSignage === true) return true; var width = Math.max(window.innerWidth || 0, 1); var height = Math.max(window.innerHeight || 0, 1); @@ -1005,7 +1949,18 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ return card; } + function syncSignageMapStageSize(root) { + var compat = getSignageCompatibilityProfile(); + if (!compat.legacy || compat.aspectRatio) return; + qa('.jrs-signage-map-stage', root || document).forEach(function(stage) { + var width = stage.clientWidth || stage.offsetWidth; + if (!width) return; + stage.style.height = Math.max(120, Math.round(width * 453 / 980)) + 'px'; + }); + } + function syncSignageMapOverlayMotion(root) { + if (getSignageCompatibilityProfile().legacy) return; var overlay = q('.jrs-signage-map-overlay', root); var sourceCard = q('.jrs-signage-left .jrs-signage-map-card.is-interactive', root); var panel = q('.jrs-signage-map-overlay-panel', root); @@ -1041,7 +1996,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ var overlay = q('.jrs-signage-map-overlay', root); if (!overlay || !overlay.classList.contains('is-visible')) return; syncSignageMapOverlayMotion(root); - window.requestAnimationFrame(function() { + raf(function() { overlay.classList.remove('is-visible'); }); } @@ -1052,7 +2007,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ syncSignageMapOverlayMotion(root); overlay.classList.remove('is-visible'); overlay.offsetWidth; - window.requestAnimationFrame(function() { + raf(function() { overlay.classList.add('is-visible'); }); } @@ -1080,8 +2035,18 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ function applySignageNativeInsets(root) { var nativeLayout = window.__jrsNativeOperationLayout || {}; if (!root) return; - root.style.setProperty('--jrs-native-safe-left', Math.max(0, Number(nativeLayout.safeAreaLeft || 0)) + 'px'); - root.style.setProperty('--jrs-native-safe-right', Math.max(0, Number(nativeLayout.safeAreaRight || 0)) + 'px'); + var safeLeft = Math.max(0, Number(nativeLayout.safeAreaLeft || 0)); + var safeRight = Math.max(0, Number(nativeLayout.safeAreaRight || 0)); + var compat = getSignageCompatibilityProfile(); + if (compat.legacy) { + root.style.paddingTop = '0px'; + root.style.paddingBottom = '10px'; + root.style.paddingLeft = (10 + safeLeft) + 'px'; + root.style.paddingRight = (10 + safeRight) + 'px'; + return; + } + root.style.setProperty('--jrs-native-safe-left', safeLeft + 'px'); + root.style.setProperty('--jrs-native-safe-right', safeRight + 'px'); } function buildSignageSignature(items, statusInfo) { @@ -1142,7 +2107,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ titleNode.textContent = item.subTitle || item.title || '運行情報'; titleNode.classList.remove('is-overflow'); titleNode.style.removeProperty('--jrs-title-overflow'); - window.requestAnimationFrame(function() { + raf(function() { var parent = titleNode.parentNode; var overflow = parent ? Math.max(0, titleNode.scrollWidth - parent.clientWidth + 18) : 0; if (overflow > 48) { @@ -1184,7 +2149,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ }, delay); } - window.requestAnimationFrame(function() { + raf(function() { var maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight); if (maxScroll < 48) { scheduleNext(2600); @@ -1217,9 +2182,14 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ function installSignageMode() { var state = getSignageState(); var nativeLayout = window.__jrsNativeOperationLayout || {}; + if (!nativeLayout.enableLandscapeMode) { + state.viewportMisses = 0; + removeSignageMode(); + return; + } if (!isSignageViewport()) { - state.viewportMisses += 1; - if (nativeLayout.forceSignage !== true && state.viewportMisses >= 3) removeSignageMode(); + state.viewportMisses = 0; + removeSignageMode(); return; } state.viewportMisses = 0; @@ -1228,8 +2198,11 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ var statusInfo = getOperationStatus(items); var signature = buildSignageSignature(items, statusInfo); var existing = q('#jrs-signage-root'); + var compat = getSignageCompatibilityProfile(); if (existing && existing.getAttribute('data-signature') === signature) { applySignageNativeInsets(existing); + existing.classList.toggle('is-legacy', compat.legacy); + syncSignageMapStageSize(existing); document.documentElement.classList.add('jrs-signage-active'); var existingState = getSignageState(); if (!existingState.scrollTimer && !existingState.dwellTimer) startSignageAutoScroll(existing); @@ -1242,6 +2215,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ var root = document.createElement('div'); root.id = 'jrs-signage-root'; + root.classList.toggle('is-legacy', compat.legacy); applySignageNativeInsets(root); root.setAttribute('data-signature', signature); root.setAttribute('data-count', String(items.length)); @@ -1263,13 +2237,15 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ left.appendChild(brand); var mapCard = appendSignageMap(left); - mapCard.classList.add('is-interactive'); - mapCard.addEventListener('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - pauseSignageAutoScroll(1600); - openSignageMapOverlay(root); - }); + if (!compat.legacy) { + mapCard.classList.add('is-interactive'); + mapCard.addEventListener('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + pauseSignageAutoScroll(1600); + openSignageMapOverlay(root); + }); + } var summary = document.createElement('div'); summary.className = 'jrs-signage-summary'; @@ -1300,7 +2276,13 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ statusIcon.alt = ''; statusAlert.appendChild(statusIcon); } else { - appendText(statusAlert, 'span', 'jrs-signage-status-alert-symbol', ''); + var compatSymbol = getSignageCompatibilityProfile(); + appendText( + statusAlert, + 'span', + 'jrs-signage-status-alert-symbol' + (compatSymbol.clipPath ? '' : ' is-fallback'), + compatSymbol.clipPath ? '' : '▲' + ); } appendText(statusAlert, 'div', 'jrs-signage-status-alert-text', statusInfo.text); status.appendChild(statusAlert); @@ -1314,7 +2296,12 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ appendText(sideList, 'div', 'jrs-signage-side-item is-active', '表示できる項目がありません'); } items.forEach(function(item, index) { - var button = appendText(sideList, 'button', 'jrs-signage-side-item', item.title || ('項目 ' + (index + 1))); + var button = appendText( + sideList, + 'button', + 'jrs-signage-side-item', + item.subTitle || item.title || ('項目 ' + (index + 1)) + ); button.type = 'button'; button.onclick = function() { setSignageIndex(index, true); }; }); @@ -1342,7 +2329,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ items.forEach(function(item, index) { var tab = appendText(tabs, 'button', 'jrs-signage-tab', String(index + 1)); tab.type = 'button'; - tab.setAttribute('aria-label', item.title || ('項目 ' + (index + 1))); + tab.setAttribute('aria-label', item.subTitle || item.title || ('項目 ' + (index + 1))); tab.onclick = function() { setSignageIndex(index, true); }; }); right.appendChild(tabs); @@ -1351,13 +2338,15 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ viewport.className = 'jrs-signage-info-viewport'; right.appendChild(viewport); - createSignageMapOverlay(root); + if (!compat.legacy) createSignageMapOverlay(root); document.body.appendChild(root); + syncSignageMapStageSize(root); document.documentElement.classList.add('jrs-signage-active'); attachSignageScrollPause(root); renderSignageActiveItem(root); } + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1373,6 +2362,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ if (!window.__jrInfoInstallTimer) { window.__jrInfoInstallTimer = window.setInterval(function() { + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1382,6 +2372,7 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ if (!window.__jrInfoObserver && window.MutationObserver) { window.__jrInfoObserver = new MutationObserver(function() { + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1403,6 +2394,12 @@ window.__TM_OPERATION_INFO_LAYOUT = Object.assign({ window.addEventListener('resize', window.__jrSignageResizeHandler); window.addEventListener('orientationchange', window.__jrSignageResizeHandler); } + + postNativeMessage({ + type: 'operationInfoReady', + href: window.location.href, + title: document.title || '' + }); })(); true; diff --git a/docs/x-operation-info-image-set-implementation-brief.md b/docs/x-operation-info-image-set-implementation-brief.md new file mode 100644 index 0000000..5e15ddd --- /dev/null +++ b/docs/x-operation-info-image-set-implementation-brief.md @@ -0,0 +1,153 @@ +# X投稿向け運行情報画像セット:実装指示書 + +## 依頼 + +`ndView.tsx` に、X(旧Twitter)へ複数画像で投稿することを前提とした運行情報画像セット生成機能を実装してください。 + +既存の「この項目を切り出す」「この小見出しを切り出す」「表示中の運行情報をすべて切り出す」「項目ごとに複数画像で共有」は残し、新たに「X投稿向け画像を作成」を追加します。 + +実装まで行い、型チェックと生成処理の確認をしてください。調査・提案だけで終了しないでください。 + +## ユーザー体験 + +新しいボタンを押すと、次の順番で最大4枚のPNGを生成し、既存の複数ファイル共有処理へ渡します。 + +1. 1枚目:運行状況の表紙(路線図+短い要約) +2. 2枚目以降:表示中の運行情報の詳細 + +Xで1枚目と2枚目が横並びに表示されたときに自然につながって見える構成にします。ただし、各画像は単体表示されても意味が通じる必要があります。画像の境界をまたいで文章や必須情報を配置しないでください。 + +## 出力仕様 + +- 各画像は `1080 x 1440 px`(3:4)の固定サイズ +- PNG +- 画像数は1〜4枚。運行情報が存在する通常ケースでは表紙+詳細で2〜4枚 +- ファイル名は投稿順を明確にする + - `operation-info-x-01-map-.png` + - `operation-info-x-02-detail-.png` + - 以下同様 +- ネイティブ側へ送る `batchIndex` はゼロ始まりの表示順と一致させる +- 既存の `Share.open({ urls })` に渡る配列順を維持する +- 画像生成途中で1枚でも失敗した場合、不完全なセットを共有せず既存形式のエラーを返す + +## 1枚目:表紙 + +必須要素: + +- `JR四国 列車運行情報` のタイトル +- 現在表示中の状態を示す短い要約 +- 四国の路線図 +- 更新日時(取得できる最新のもの) +- 詳細ページがある場合は `詳細は次の画像へ →` +- 非公式アプリによる生成画像であり、最新情報はJR四国公式を確認する旨 +- ページ番号(例:`1/4`) + +路線図は既存の `buildMapImage()` と `img/map/*` を再利用します。現在ページ上で有効になっている路線レイヤーを反映してください。既存挙動を壊さない範囲で、影響路線を目立たせ、影響のない部分を弱められるなら実装してください。状態と路線の対応を正確に判定できない場合は、誤った色分けを推測せず、現在表示されているレイヤー表現を使用してください。 + +表紙は路線図を主役にし、要約文が長い場合も地図を極端に小さくしないでください。要約は路線名・状態を中心に短く整形し、詳細本文を表紙へ詰め込みません。 + +## 2枚目以降:詳細ページ + +必須要素: + +- 路線・項目タイトル +- サブタイトル/状態 +- 更新日時 +- 小見出しと本文 +- ページ番号 +- 非公式画像の注意書き + +既存の `getOperationItems()`、`parseDetailBlocks()`、`splitCaptureBlocksIntoSections()` のデータ構造を再利用します。 + +詳細は「1項目=必ず1画像」ではなく、固定高のページへ順に組版してください。 + +- 1件が短ければ、同じページに次の項目も配置してよい +- 1件が長ければ、ブロックまたは小見出し単位で次ページへ送る +- 小見出しだけをページ末尾に残さない +- 本文を文字単位で不自然に分断するより、小見出し単位の改ページを優先する +- 4枚を超える場合は、詳細3ページへ読みやすく収める +- それでも収まらない場合は、文字を極端に縮小したり内容を黙って欠落させたりしない + +4枚で収まらないケースについては、実装上安全な扱いを選び、コードコメントと完了報告で明示してください。推奨は、最小可読サイズを守ったうえで4枚では収まらないことをユーザーへ知らせ、既存の項目別共有を案内することです。 + +## レイアウト規則 + +- 背景は白を基本とし、既存のJR四国風配色(`#0099CB` など)を継承 +- 左右80px程度をX側のクロップに備えた安全領域とする +- 必須情報は端へ寄せない +- 全ページでヘッダー、ページ番号、フッターの位置を統一 +- 本文の可読性を優先し、既存画像と同程度の文字サイズを維持 +- 1枚目右側の矢印などで2枚目への視線誘導を作ってよいが、その装飾が切れても意味が失われないようにする +- 角丸や画像間の隙間はX側が付けるため、画像自体には外周の角丸を焼き込まない + +## 既存コードの入口 + +主な対象は `ndView.tsx` です。 + +- `buildOperationPageScript()`:WebViewへ注入する生成処理 +- `buildMapImage()`:路線図Canvas合成 +- `appendCaptureItemLayout()` / `drawCaptureLayout()`:既存組版・描画 +- `getOperationItems()`:表示中の運行情報抽出 +- `installPageCaptureButton()`:共有ボタン追加 +- `renderAllItemsToSeparateImages()`:既存バッチ送信の参考 +- `saveOperationCaptureBatchItem()`:ネイティブ側で順番どおりにファイルを集約 +- `shareOperationCaptureFiles()`:単一/複数ファイル共有 + +必要ならX投稿向けのレイアウト計算・描画関数を独立させてください。既存の可変幅・可変高画像生成ロジックへ多数の条件分岐を足すより、データ抽出と基本描画だけ共有する構成を優先します。 + +## 実装上の制約 + +- 既存の切り出し/共有機能を壊さない +- 横画面サイネージモードでは、従来どおりキャプチャボタンを表示しない +- iOS・Android双方で動く既存のWebViewメッセージ方式を維持 +- 新しい依存パッケージは原則追加しない +- WebView内で利用できない新しいJavaScript構文を安易に導入しない +- 生成中の二重タップで複数バッチが競合しないよう考慮する +- ユーザーの既存変更があるファイルを勝手に巻き戻さない +- `scripts/generate-operation-info-userscript.mjs` が `ndView.tsx` の注入コードを元に生成物を作るため、必要なら生成物も更新する。ただし生成物を直接手編集しない + +## UI文言 + +新規ボタン: + +`X投稿向け画像を作成` + +生成・共有に失敗した場合は、既存の「切り出し失敗」系ダイアログとトーンをそろえます。4枚へ収まらない場合は、理由と代替手段が分かる日本語を表示してください。 + +## 完了条件 + +- 新規ボタンから表紙+詳細画像が正しい順番で共有される +- 全画像が厳密に1080×1440 +- 1件、2件、3件、4件以上、長文1件のレイアウトを確認している +- 長文がCanvas外へはみ出さない +- 空のタイトル、サブタイトル、更新日時でも壊れない +- 地図画像のロード失敗時も白紙画像を共有せず、明確に失敗扱いまたは安全なフォールバックになる +- 既存4種類の切り出し操作が維持される +- TypeScript型チェックが通る +- `git diff --check` が通る + +## 検証 + +最低限、次を実行してください。 + +```bash +npx tsc --noEmit +git diff --check +``` + +注入スクリプトを変更した場合は、リポジトリ既存の生成手順も確認します。 + +```bash +node scripts/generate-operation-info-userscript.mjs +``` + +可能なら、Canvas描画を開発用に呼び出せる小さなテスト入口または純粋なページ分割関数を用意し、上記の件数・長文パターンを検証してください。本番UIへデバッグ操作を残さないでください。 + +## 完了報告に含めるもの + +- 変更したファイル +- 表紙・詳細ページの分割ルール +- 4枚で収まらない場合の扱い +- 実行した検証と結果 +- 端末で確認が必要な残件 + diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index ae567c0..07afe47 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -997,7 +997,7 @@ export const injectJavascriptData = ({ badgeHtml += "
"; } - 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainTypeHtml + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); + 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + viaData + "

" + optionalText + "

" + trainName + "

" + (ToData ? ToData + "行" : ToData) + "

" + trainTypeHtml + "

" + (hasProblem ? "‼️停止中‼️" : "") + "

"); `: ` var _defTextColor = _isDark ? 'white' : 'black'; 行き先情報.insertAdjacentHTML('beforebegin', "

" + returnText1 + "

"); diff --git a/ndView.tsx b/ndView.tsx index e1432c8..7de4153 100644 --- a/ndView.tsx +++ b/ndView.tsx @@ -247,6 +247,9 @@ const buildOperationPageScript = (layout: { '.jrs-capture-page-link:active{opacity:.9 !important;transform:translateY(1px) !important;}', '.jrs-capture-page-link.is-secondary{background:#ffffff !important;color:#0076a8 !important;border:1px solid #0099CB !important;box-shadow:none !important;}', '.jrs-capture-page-link.is-secondary:visited{color:#0076a8 !important;}', + '.jrs-capture-page-link.is-x{background:linear-gradient(135deg,#111827 0%,#0f76a8 100%) !important;color:#fff !important;border:none !important;box-shadow:0 6px 14px rgba(15,118,168,.24) !important;}', + '.jrs-capture-page-link.is-x:visited{color:#fff !important;}', + '.jrs-capture-page-link.is-disabled,.jrs-capture-link.is-disabled,.jrs-subcapture-link.is-disabled{pointer-events:none !important;opacity:.52 !important;transform:none !important;}', '.jrs-capture-wrap{display:block !important;text-align:right !important;margin:12px 0 8px !important;}', '.jrs-capture-link{display:inline-block !important;background:#0a84ff !important;color:#fff !important;padding:10px 14px !important;border-radius:999px !important;font-size:12px !important;font-weight:700 !important;line-height:1.2 !important;text-decoration:none !important;box-shadow:0 4px 12px rgba(10,132,255,.25) !important;position:relative !important;z-index:9999 !important;}', '.jrs-capture-link:visited{color:#fff !important;}', @@ -394,8 +397,38 @@ const buildOperationPageScript = (layout: { } function postMessage(payload) { + if (!window.ReactNativeWebView) return; payload.type = 'operationInfoCapture'; - postNativeMessage(payload); + window.ReactNativeWebView.postMessage(JSON.stringify(payload)); + } + + function setCaptureButtonsBusy(isBusy) { + qa('.jrs-capture-page-link, .jrs-capture-link, .jrs-subcapture-link').forEach(function(element) { + if (!element || !element.classList) return; + element.classList[isBusy ? 'add' : 'remove']('is-disabled'); + element.setAttribute('aria-disabled', isBusy ? 'true' : 'false'); + }); + } + + function runCaptureAction(action) { + if (window.__jrCaptureBusy) { + return; + } + + window.__jrCaptureBusy = true; + setCaptureButtonsBusy(true); + + Promise.resolve() + .then(action) + .catch(function() { + postMessage({ error: true }); + }) + .then(function() { + window.setTimeout(function() { + window.__jrCaptureBusy = false; + setCaptureButtonsBusy(false); + }, 120); + }); } function parseDetailBlocks(html) { @@ -660,6 +693,518 @@ const buildOperationPageScript = (layout: { }); } + + var X_CAPTURE_PAGE_WIDTH = 1080; + var X_CAPTURE_PAGE_HEIGHT = 1440; + var X_CAPTURE_SAFE_X = 80; + var X_CAPTURE_HEADER_TOP = 76; + var X_CAPTURE_CONTENT_TOP = 208; + var X_CAPTURE_CONTENT_BOTTOM = 1218; + var X_CAPTURE_FOOTER_Y = 1262; + var X_CAPTURE_ITEM_GAP = 26; + var X_CAPTURE_UNIT_GAP = 18; + + function getXCaptureNotice() { + return 'この画像は非公式アプリで生成したものです。最新情報・詳細はJR四国公式の運行情報をご確認ください。'; + } + + function getXPageNumberText(pageIndex, totalPages) { + return String(pageIndex + 1) + '/' + String(totalPages); + } + + function parseUpdatedAtValue(value) { + var text = strip(value); + if (!text) return null; + var normalized = text + .replace(/更新日時[::]?/g, '') + .replace(/年/g, '/') + .replace(/月/g, '/') + .replace(/日/g, '') + .replace(/時/g, ':') + .replace(/分/g, '') + .replace(/\./g, '/') + .replace(/-/g, '/') + .trim(); + var parsed = Date.parse(normalized); + return isNaN(parsed) ? null : parsed; + } + + function getLatestUpdatedAt(items) { + var bestText = ''; + var bestValue = null; + (items || []).forEach(function(item) { + var updatedAt = strip(item && item.updatedAt); + if (!updatedAt) return; + var parsed = parseUpdatedAtValue(updatedAt); + if (parsed == null) { + if (!bestText) bestText = updatedAt; + return; + } + if (bestValue == null || parsed > bestValue) { + bestValue = parsed; + bestText = updatedAt; + } + }); + return bestText; + } + + function buildXCoverSummary(items) { + var entries = (items || []).map(function(item) { + var title = strip(item && item.title) || '運行情報'; + var subTitle = strip(item && item.subTitle); + return subTitle ? title + ':' + subTitle : title; + }).filter(function(text) { + return !!text; + }); + + if (!entries.length) { + return '現在表示中の運行情報はありません。'; + } + + if (entries.length === 1) { + return entries[0]; + } + + if (entries.length === 2) { + return entries[0] + ' / ' + entries[1]; + } + + return entries.slice(0, 3).join(' / ') + (entries.length > 3 ? ' ほか' : ''); + } + + function measureXDetailHeader(ctx, item, contentWidth, continued) { + var textWidth = contentWidth - 54; + ctx.font = "800 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var titleLines = wrapText(ctx, strip(item.title) || '運行情報', textWidth); + ctx.font = "700 26px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var subTitleLines = strip(item.subTitle) ? wrapText(ctx, item.subTitle, textWidth) : []; + ctx.font = "500 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var updatedLines = strip(item.updatedAt) ? wrapText(ctx, item.updatedAt, textWidth) : []; + var height = 30 + titleLines.length * 44 + (subTitleLines.length ? 10 + subTitleLines.length * 32 : 0) + (updatedLines.length ? 12 + updatedLines.length * 28 : 0) + 24; + return { + titleLines: titleLines, + subTitleLines: subTitleLines, + updatedLines: updatedLines, + continued: !!continued, + height: Math.max(118, height) + }; + } + + function createXDetailUnits(ctx, item, bodyWidth) { + var units = []; + var pendingHeading = ''; + var blocks = item && item.blocks ? item.blocks : []; + + blocks.forEach(function(block) { + if (block.type === 'badge') { + pendingHeading = strip(block.text); + return; + } + + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var bodyLines = wrapText(ctx, strip(block.text), bodyWidth); + if (!bodyLines.length && !pendingHeading) return; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var headingLines = pendingHeading ? wrapText(ctx, pendingHeading, bodyWidth - 24) : []; + units.push({ + headingLines: headingLines, + bodyLines: bodyLines, + headingText: pendingHeading, + lineHeight: 39 + }); + pendingHeading = ''; + }); + + if (!units.length) { + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + units.push({ + headingLines: [], + bodyLines: wrapText(ctx, '詳細情報はJR四国公式の運行情報をご確認ください。', bodyWidth), + headingText: '', + lineHeight: 39 + }); + } + + return units; + } + + function getXUnitHeight(unit) { + var headingHeight = unit.headingLines && unit.headingLines.length ? Math.max(56, unit.headingLines.length * 35 + 20) : 0; + var bodyHeight = Math.max(unit.bodyLines.length, 1) * unit.lineHeight; + return 22 + headingHeight + (headingHeight ? 14 : 0) + bodyHeight + 18; + } + + function buildXUnitChunk(unit, availableHeight) { + var fullHeight = getXUnitHeight(unit); + if (fullHeight <= availableHeight) { + return { + chunk: { + headingLines: unit.headingLines.slice(), + bodyLines: unit.bodyLines.slice(), + lineHeight: unit.lineHeight, + height: fullHeight + }, + consumed: true, + rest: null, + height: fullHeight + }; + } + + var headingHeight = unit.headingLines && unit.headingLines.length ? Math.max(56, unit.headingLines.length * 35 + 20) : 0; + var availableForBody = availableHeight - 22 - headingHeight - (headingHeight ? 14 : 0) - 18; + var lineCapacity = Math.floor(availableForBody / unit.lineHeight); + var minimumLines = headingHeight ? 2 : 3; + if (lineCapacity < minimumLines) { + return null; + } + + var firstLines = unit.bodyLines.slice(0, lineCapacity); + if (!firstLines.length) { + return null; + } + + var restLines = unit.bodyLines.slice(lineCapacity); + var chunk = { + headingLines: unit.headingLines.slice(), + bodyLines: firstLines, + lineHeight: unit.lineHeight + }; + chunk.height = getXUnitHeight(chunk); + + return { + chunk: chunk, + consumed: restLines.length === 0, + rest: restLines.length ? { + headingLines: [], + bodyLines: restLines, + lineHeight: unit.lineHeight + } : null, + height: chunk.height + }; + } + + function createEmptyXDetailPage() { + return { items: [], usedHeight: 0 }; + } + + function paginateXDetailPages(ctx, items, contentWidth) { + var bodyWidth = contentWidth - 32; + var maxHeight = X_CAPTURE_CONTENT_BOTTOM - X_CAPTURE_CONTENT_TOP; + var pages = [createEmptyXDetailPage()]; + + for (var itemIndex = 0; itemIndex < items.length; itemIndex += 1) { + var item = items[itemIndex]; + var units = createXDetailUnits(ctx, item, bodyWidth); + var unitIndex = 0; + var continued = false; + + while (unitIndex < units.length) { + var page = pages[pages.length - 1]; + var gapBeforeItem = page.items.length ? X_CAPTURE_ITEM_GAP : 0; + var header = measureXDetailHeader(ctx, item, contentWidth, continued); + var availableForStart = maxHeight - page.usedHeight - gapBeforeItem - header.height; + var preview = buildXUnitChunk(units[unitIndex], availableForStart); + + if (!preview) { + if (page.items.length) { + pages.push(createEmptyXDetailPage()); + continue; + } + return null; + } + + var pageItem = { + header: header, + units: [] + }; + if (gapBeforeItem) { + page.usedHeight += gapBeforeItem; + } + page.items.push(pageItem); + page.usedHeight += header.height; + + while (unitIndex < units.length) { + var availableHeight = maxHeight - page.usedHeight; + var chunkInfo = buildXUnitChunk(units[unitIndex], availableHeight); + if (!chunkInfo) { + break; + } + + pageItem.units.push(chunkInfo.chunk); + page.usedHeight += chunkInfo.height; + + if (chunkInfo.consumed) { + unitIndex += 1; + } else if (chunkInfo.rest) { + units[unitIndex] = chunkInfo.rest; + } + + if (unitIndex < units.length) { + page.usedHeight += X_CAPTURE_UNIT_GAP; + } + } + + if (unitIndex < units.length) { + pages.push(createEmptyXDetailPage()); + continued = true; + } + } + } + + return pages.filter(function(page) { + return page.items.length > 0; + }); + } + + function drawXPageChrome(ctx, pageIndex, totalPages, subHeading) { + var titleX = X_CAPTURE_SAFE_X; + ctx.fillStyle = '#0099CB'; + ctx.fillRect(titleX, X_CAPTURE_HEADER_TOP + 4, 12, 76); + ctx.fillStyle = '#0f1720'; + ctx.font = "800 56px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('JR四国 列車運行情報', titleX + 28, X_CAPTURE_HEADER_TOP + 57); + ctx.fillStyle = '#4b5563'; + ctx.font = "700 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText(subHeading, titleX + 30, X_CAPTURE_HEADER_TOP + 96); + ctx.fillStyle = '#0f1720'; + ctx.font = "700 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var pageText = getXPageNumberText(pageIndex, totalPages); + var pageTextWidth = ctx.measureText(pageText).width; + ctx.fillText(pageText, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X - pageTextWidth, X_CAPTURE_HEADER_TOP + 52); + } + + function drawXFooter(ctx, pageIndex, totalPages) { + var notice = getXCaptureNotice(); + ctx.strokeStyle = '#d6dde5'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y - 18); + ctx.lineTo(X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y - 18); + ctx.stroke(); + ctx.fillStyle = '#5b6470'; + ctx.font = "400 20px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + wrapText(ctx, notice, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 90).slice(0, 3).forEach(function(line, index) { + ctx.fillText(line, X_CAPTURE_SAFE_X, X_CAPTURE_FOOTER_Y + index * 26); + }); + ctx.fillStyle = '#111827'; + ctx.font = "800 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var pageText = getXPageNumberText(pageIndex, totalPages); + var pageWidth = ctx.measureText(pageText).width; + ctx.fillText(pageText, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X - pageWidth, X_CAPTURE_FOOTER_Y + 52); + } + + function drawXDetailHeaderBlock(ctx, header, x, y, width) { + ctx.fillStyle = '#0099CB'; + ctx.fillRect(x, y, width, header.height); + ctx.fillStyle = '#ffffff'; + ctx.font = "800 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var textY = y + 44; + header.titleLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 44; + }); + if (header.subTitleLines.length) { + ctx.font = "700 26px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + textY += 2; + header.subTitleLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 32; + }); + } + if (header.updatedLines.length) { + ctx.font = "500 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + textY += 4; + header.updatedLines.forEach(function(line) { + ctx.fillText(line, x + 22, textY); + textY += 28; + }); + } + if (header.continued) { + ctx.fillStyle = 'rgba(255,255,255,.22)'; + ctx.fillRect(x + width - 152, y + 18, 124, 34); + ctx.fillStyle = '#ffffff'; + ctx.font = "700 20px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('続き', x + width - 109, y + 41); + } + } + + function drawXUnitBlock(ctx, unit, x, y, width) { + ctx.strokeStyle = '#d7dee7'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, unit.height); + var textY = y + 34; + if (unit.headingLines.length) { + var headingHeight = Math.max(56, unit.headingLines.length * 35 + 20); + ctx.fillStyle = '#f3f8fb'; + ctx.fillRect(x, y, width, headingHeight + 20); + ctx.fillStyle = '#0099CB'; + ctx.fillRect(x + 14, y + 12, 8, headingHeight - 4); + ctx.fillStyle = '#0f1720'; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + unit.headingLines.forEach(function(line) { + ctx.fillText(line, x + 34, textY); + textY += 35; + }); + textY = y + headingHeight + 34; + } + ctx.fillStyle = '#111827'; + ctx.font = "400 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + unit.bodyLines.forEach(function(line) { + ctx.fillText(line, x + 18, textY); + textY += unit.lineHeight; + }); + } + + async function buildXCoverPage(items, totalPages, pageIndex) { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + if (!ctx) return null; + + canvas.width = X_CAPTURE_PAGE_WIDTH; + canvas.height = X_CAPTURE_PAGE_HEIGHT; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + drawXPageChrome(ctx, pageIndex, totalPages, 'X投稿向け画像'); + + var summary = buildXCoverSummary(items); + ctx.font = "700 38px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + var summaryLines = wrapText(ctx, summary, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 44).slice(0, 4); + var summaryHeight = Math.max(138, 32 + summaryLines.length * 46); + ctx.fillStyle = '#0f76a8'; + ctx.fillRect(X_CAPTURE_SAFE_X, 186, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2, summaryHeight); + ctx.fillStyle = '#ffffff'; + summaryLines.forEach(function(line, index) { + ctx.fillText(line, X_CAPTURE_SAFE_X + 24, 236 + index * 46); + }); + + var mapCanvas; + try { + mapCanvas = await buildMapImage(X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2 - 30); + } catch (e) { + return null; + } + if (!mapCanvas) return null; + + var mapCardY = 186 + summaryHeight + 34; + var mapX = X_CAPTURE_SAFE_X + 15; + var mapY = mapCardY + 16; + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = '#c7dcea'; + ctx.lineWidth = 2; + ctx.strokeRect(X_CAPTURE_SAFE_X, mapCardY, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2, mapCanvas.height + 32); + ctx.drawImage(mapCanvas, mapX, mapY, mapCanvas.width, mapCanvas.height); + + var latestUpdatedAt = getLatestUpdatedAt(items); + var infoY = mapCardY + mapCanvas.height + 72; + ctx.fillStyle = '#0f1720'; + ctx.font = "700 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('現在表示中の路線図と運行情報', X_CAPTURE_SAFE_X, infoY); + if (latestUpdatedAt) { + ctx.fillStyle = '#4b5563'; + ctx.font = "500 24px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText(latestUpdatedAt, X_CAPTURE_SAFE_X, infoY + 40); + } + if (totalPages > 1) { + ctx.fillStyle = '#0099CB'; + ctx.font = "800 30px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; + ctx.fillText('詳細は次の画像へ →', X_CAPTURE_SAFE_X, infoY + 94); + } + + drawXFooter(ctx, pageIndex, totalPages); + return canvas; + } + + function buildXDetailPage(page, pageIndex, totalPages) { + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + if (!ctx) return null; + + canvas.width = X_CAPTURE_PAGE_WIDTH; + canvas.height = X_CAPTURE_PAGE_HEIGHT; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawXPageChrome(ctx, pageIndex, totalPages, '運行情報詳細'); + + var y = X_CAPTURE_CONTENT_TOP; + var width = X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2; + page.items.forEach(function(pageItem, itemIndex) { + if (itemIndex > 0) { + y += X_CAPTURE_ITEM_GAP; + } + drawXDetailHeaderBlock(ctx, pageItem.header, X_CAPTURE_SAFE_X, y, width); + y += pageItem.header.height; + pageItem.units.forEach(function(unit, unitIndex) { + drawXUnitBlock(ctx, unit, X_CAPTURE_SAFE_X, y, width); + y += unit.height; + if (unitIndex < pageItem.units.length - 1) { + y += X_CAPTURE_UNIT_GAP; + } + }); + }); + + drawXFooter(ctx, pageIndex, totalPages); + return canvas; + } + + async function renderXPostImageSet(items, fileNameBase) { + try { + if (!items || !items.length) { + postMessage({ error: true, reason: 'x-empty' }); + return; + } + + var measureCanvas = document.createElement('canvas'); + var measureCtx = measureCanvas.getContext('2d'); + if (!measureCtx) { + postMessage({ error: true }); + return; + } + + var detailPages = paginateXDetailPages(measureCtx, items, X_CAPTURE_PAGE_WIDTH - X_CAPTURE_SAFE_X * 2); + if (!detailPages || detailPages.length > 3) { + postMessage({ error: true, reason: 'x-overflow' }); + return; + } + + var totalPages = 1 + detailPages.length; + var timestamp = strip(fileNameBase) || String(Date.now()); + var cover = await buildXCoverPage(items, totalPages, 0); + if (!cover) { + postMessage({ error: true, reason: 'x-map' }); + return; + } + + var pages = [cover]; + detailPages.forEach(function(page, index) { + var detailCanvas = buildXDetailPage(page, index + 1, totalPages); + if (detailCanvas) { + pages.push(detailCanvas); + } + }); + + if (pages.length !== totalPages) { + postMessage({ error: true }); + return; + } + + var batchId = 'operation-info-x-batch-' + Date.now() + '-' + Math.floor(Math.random() * 100000); + for (var index = 0; index < pages.length; index += 1) { + postMessage({ + batchId: batchId, + batchIndex: index, + batchTotal: pages.length, + dataUrl: pages[index].toDataURL('image/png'), + fileName: index === 0 + ? 'operation-info-x-01-map-' + timestamp + '.png' + : 'operation-info-x-0' + String(index + 1) + '-detail-' + timestamp + '.png' + }); + } + } catch (e) { + postMessage({ error: true }); + } + } + function appendCaptureSectionLayout(layout, ctx, section, startY, options) { var boxX = options && options.x != null ? options.x : 0; var boxY = startY; @@ -1105,20 +1650,22 @@ const buildOperationPageScript = (layout: { link.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - var updatedAtNode = q('.upd_time', dd); - var subTitleNode = q('.delay_subttl', heading); - var sectionHtml = getSubsectionHtml(midasi); - if (!sectionHtml) { - postMessage({ error: true }); - return; - } - renderItemToImage( - getTitleText(heading), - subTitleNode ? subTitleNode.textContent : '', - updatedAtNode ? updatedAtNode.textContent : '', - sectionHtml, - 'operation-info-' + infoId + '-section-' + index + '-' + Date.now() - ); + runCaptureAction(function() { + var updatedAtNode = q('.upd_time', dd); + var subTitleNode = q('.delay_subttl', heading); + var sectionHtml = getSubsectionHtml(midasi); + if (!sectionHtml) { + postMessage({ error: true }); + return; + } + return renderItemToImage( + getTitleText(heading), + subTitleNode ? subTitleNode.textContent : '', + updatedAtNode ? updatedAtNode.textContent : '', + sectionHtml, + 'operation-info-' + infoId + '-section-' + index + '-' + Date.now() + ); + }); }; wrap.appendChild(link); if (midasi.nextSibling) { @@ -1157,7 +1704,9 @@ const buildOperationPageScript = (layout: { allLink.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now()); + runCaptureAction(function() { + return renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now()); + }); }; wrap.appendChild(allLink); } @@ -1172,13 +1721,31 @@ const buildOperationPageScript = (layout: { batchLink.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - renderAllItemsToSeparateImages(getOperationItems(), 'operation-info-share-' + Date.now()); + runCaptureAction(function() { + return renderAllItemsToSeparateImages(getOperationItems(), 'operation-info-share-' + Date.now()); + }); }; wrap.appendChild(batchLink); } } else if (batchLink && batchLink.parentNode) { batchLink.parentNode.removeChild(batchLink); } + + var xLink = q('.jrs-capture-page-link.is-x', wrap); + if (!xLink) { + xLink = document.createElement('a'); + xLink.href = '#'; + xLink.className = 'jrs-capture-page-link is-x'; + xLink.textContent = 'X投稿向け画像を作成'; + xLink.onclick = function(event) { + event.preventDefault(); + event.stopPropagation(); + runCaptureAction(function() { + return renderXPostImageSet(getOperationItems(), String(Date.now())); + }); + }; + wrap.appendChild(xLink); + } } function removeCaptureButtons() { @@ -1216,19 +1783,21 @@ const buildOperationPageScript = (layout: { link.onclick = function(event) { event.preventDefault(); event.stopPropagation(); - var updatedAtNode = q('.upd_time', dd); - var subTitleNode = q('.delay_subttl', heading); - if (!detailNode.innerHTML) { - postMessage({ error: true }); - return; - } - renderItemToImage( - getTitleText(heading), - subTitleNode ? subTitleNode.textContent : '', - updatedAtNode ? updatedAtNode.textContent : '', - detailNode.innerHTML, - 'operation-info-' + infoId + '-' + Date.now() - ); + runCaptureAction(function() { + var updatedAtNode = q('.upd_time', dd); + var subTitleNode = q('.delay_subttl', heading); + if (!detailNode.innerHTML) { + postMessage({ error: true }); + return; + } + return renderItemToImage( + getTitleText(heading), + subTitleNode ? subTitleNode.textContent : '', + updatedAtNode ? updatedAtNode.textContent : '', + detailNode.innerHTML, + 'operation-info-' + infoId + '-' + Date.now() + ); + }); }; wrap.appendChild(link); dd.insertBefore(wrap, dd.firstChild); @@ -2633,7 +3202,12 @@ export default function tndView() { }, }); if (payload.error) { - Alert.alert("切り出し失敗", "項目画像の生成に失敗しました。"); + const message = payload.reason === "x-overflow" + ? "X投稿向け画像は4枚以内に読みやすく収まりませんでした。『項目ごとに複数画像で共有』をご利用ください。" + : payload.reason === "x-map" + ? "路線図の読み込みに失敗したため、X投稿向け画像を生成できませんでした。" + : "項目画像の生成に失敗しました。"; + Alert.alert("切り出し失敗", message); return; } if (payload.batchId) {