import React, { useRef, useEffect, useCallback, useMemo } from "react"; import { Alert, Animated, View, Platform, TouchableOpacity, StyleProp, ViewStyle, Linking, AppState, useWindowDimensions, } from "react-native"; import * as FileSystem from "expo-file-system/legacy"; import * as Sharing from "expo-sharing"; import { activateKeepAwakeAsync, deactivateKeepAwake, } from "expo-keep-awake"; import { WebView } from "react-native-webview"; import { Ionicons } from "@expo/vector-icons"; import { useNavigation, useFocusEffect } from "@react-navigation/native"; import { useThemeColors } from "@/lib/theme"; import { AS } from "./storageControl"; import { STORAGE_KEYS } from "@/constants"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useWebViewRemount } from "@/lib/useWebViewRemount"; import { useAreaInfo } from "./stateBox/useAreaInfo"; import lineColorList from "./assets/originData/lineColorList"; const KEEP_AWAKE_TAG = "operation-info-landscape"; const isActivityUnavailableError = (error: unknown) => String(error).includes("The current activity is no longer available"); const buildOperationPageScript = (layout: { isLandscape: boolean; safeAreaLeft: number; safeAreaRight: number; lineBadges: Array<{ code: string; color: string }>; enableLandscapeMode: boolean; enableCapture: boolean; }) => { const nativeLayout = JSON.stringify({ forceSignage: layout.enableLandscapeMode && layout.isLandscape, enableLandscapeMode: layout.enableLandscapeMode, enableCapture: layout.enableCapture, safeAreaLeft: Math.max(0, Math.round(layout.safeAreaLeft)), safeAreaRight: Math.max(0, Math.round(layout.safeAreaRight)), lineBadges: layout.lineBadges, }); return String.raw` (function() { window.__jrsNativeOperationLayout = ${nativeLayout}; function q(selector, root) { return (root || document).querySelector(selector); } function qa(selector, root) { return Array.prototype.slice.call((root || document).querySelectorAll(selector)); } function strip(value) { return String(value || '').replace(/\s+/g, ' ').trim(); } function removeNodes(root, selector) { qa(selector, root).forEach(function(node) { if (node && node.parentNode) node.parentNode.removeChild(node); }); } function getStatusText(status) { if (!status) return ''; var clone = status.cloneNode(true); removeNodes(clone, 'img, a, button, input, select, textarea, svg, script, style'); return strip(clone.textContent).replace(/\s*詳細\s*$/, ''); } function getNativeLineBadges() { var nativeLayout = window.__jrsNativeOperationLayout || {}; var seen = {}; return (Array.isArray(nativeLayout.lineBadges) ? nativeLayout.lineBadges : []) .map(function(entry) { return { code: strip(entry && entry.code).toUpperCase(), color: strip(entry && entry.color) || '#5f6b78' }; }) .filter(function(entry) { if (!/^[A-Z]$/.test(entry.code) || seen[entry.code]) return false; seen[entry.code] = true; return true; }); } function getContrastTextColor(hexColor) { var color = String(hexColor || '').replace('#', ''); if (!/^[0-9a-fA-F]{6}$/.test(color)) return '#ffffff'; var r = parseInt(color.slice(0, 2), 16); var g = parseInt(color.slice(2, 4), 16); var b = parseInt(color.slice(4, 6), 16); var luminance = (r * 299 + g * 587 + b * 114) / 1000; return luminance >= 160 ? '#0f1720' : '#ffffff'; } function setStyles(selector, styles) { qa(selector).forEach(function(element) { Object.keys(styles).forEach(function(key) { element.style[key] = styles[key]; }); }); } function applyLayoutTweaks() { setStyles('.sitettl', { display: 'none' }); setStyles('.attention', { display: 'none', width: '100vw' }); setStyles('.mapheader', { display: 'none' }); setStyles('.map', { width: '100vw', display: 'block', marginLeft: '-5px', marginTop: '0px' }); setStyles('.pageInformation h1.accent span', { width: '100%' }); setStyles('.delay_status', { width: 'calc(100vw - 4px)' }); setStyles('.reload', { width: '100vw' }); setStyles('.delay_info', { width: '100vw' }); setStyles('.related_lnk', { width: '100%', padding: '0px' }); setStyles('.related_lnk .ttl', { padding: '20px' }); setStyles('.related_lnk dl.lnk_item', { float: 'none', overflow: 'visible', width: '100%' }); setStyles('.mapbase > img', { width: '100vw' }); setStyles('.ml_station > img', { width: '100vw' }); [ ['.ml_seto', '14.5vw', { top: '0.2vw', right: '25.8vw' }], ['.ml_yosan1', '42.6vw', { top: '5.1vw', left: '18.7vw' }], ['.ml_yosan2', '12.2vw', { top: '8.6vw', left: '8.0vw' }], ['.ml_dosan1', '13.5vw', { top: '6.6vw', left: '47.8vw' }], ['.ml_dosan2', '22.5vw', { top: '32vw', left: '26.5vw' }], ['.ml_kotoku', '17vw', { top: '6.5vw', left: '72.9vw' }], ['.ml_mugi', '11.6vw', { top: '23.6vw', left: '78.3vw' }], ['.ml_tokushima', '27.8vw', { top: '17vw', left: '61.3vw' }], ['.ml_naruto', '5.5vw', { top: '17.1vw', left: '88.5vw' }], ['.ml_yodo', '18.3vw', { top: '30.2vw', left: '9.6vw' }] ].forEach(function(entry) { var selector = entry[0]; var width = entry[1]; var position = entry[2]; var image = q(selector + ' > img'); var layer = q(selector); if (image) { image.removeAttribute('width'); image.removeAttribute('height'); image.style.width = width; } if (layer) { Object.keys(position).forEach(function(key) { layer.style[key] = position[key]; }); } }); } function ensureStyles() { var style = q('#jrs-operation-capture-style'); if (!style) { style = document.createElement('style'); 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;}', '.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-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;}', '.jrs-capture-link:active{opacity:.9 !important;transform:translateY(1px) !important;}', '.jrs-capture-link-debug{outline:2px solid red !important;}', '.jrs-subcapture-wrap{display:block !important;text-align:right !important;margin:8px 0 10px !important;}', '.jrs-subcapture-link{display:inline-block !important;background:#ffffff !important;color:#0076a8 !important;border:1px solid #0099CB !important;padding:7px 11px !important;border-radius:999px !important;font-size:11px !important;font-weight:700 !important;line-height:1.2 !important;text-decoration:none !important;position:relative !important;z-index:9999 !important;}', '.jrs-subcapture-link:visited{color:#0076a8 !important;}', '.jrs-subcapture-link:active{opacity:.85 !important;}', 'html.jrs-signage-active,html.jrs-signage-active body{overflow:hidden !important;background:#0099CC !important;}', '#jrs-signage-root{position:fixed !important;inset:0 !important;z-index:9998 !important;box-sizing:border-box !important;padding:0 calc(clamp(4px,.75vw,10px) + var(--jrs-native-safe-right,0px)) clamp(4px,.75vw,10px) calc(clamp(4px,.75vw,10px) + var(--jrs-native-safe-left,0px)) !important;background:#0099CC !important;color:#fff !important;font-family:-apple-system,BlinkMacSystemFont,"Helvetica Neue","Segoe UI",sans-serif !important;overflow:hidden !important;}', '#jrs-signage-root *{box-sizing:border-box !important;}', '.jrs-signage-shell{height:100% !important;display:grid !important;grid-template-columns:minmax(230px,28%) minmax(0,1fr) !important;gap:clamp(5px,.75vw,10px) !important;}', '.jrs-signage-left{min-width:0 !important;display:flex !important;flex-direction:column !important;gap:clamp(8px,.95vw,12px) !important;overflow:hidden !important;min-height:0 !important;}', '.jrs-signage-brand{display:flex !important;align-items:center !important;gap:10px !important;padding:clamp(8px,.9vw,12px) clamp(12px,1.15vw,16px) !important;color:#fff !important;font-weight:900 !important;font-size:clamp(19px,2vw,34px) !important;line-height:1 !important;border-radius:12px !important;background:#0b92c4 !important;border:1px solid rgba(255,255,255,.16) !important;box-shadow:0 2px 6px rgba(0,54,80,.10) !important;}', '.jrs-signage-brand:before{content:"" !important;width:clamp(8px,.75vw,12px) !important;height:clamp(26px,2.5vw,42px) !important;background:#ffffff !important;border-radius:999px !important;display:block !important;flex:0 0 auto !important;}', '.jrs-signage-clock{margin-left:auto !important;border:1px solid rgba(255,255,255,.24) !important;border-radius:10px !important;padding:5px 10px !important;color:#fff !important;font-size:clamp(11px,.95vw,16px) !important;font-weight:800 !important;white-space:nowrap !important;background:rgba(255,255,255,.14) !important;}', '.jrs-signage-map-card{background:#fff !important;border:2px solid #000 !important;border-radius:5px !important;padding:clamp(4px,.65vw,8px) !important;overflow:hidden !important;transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease !important;}', '.jrs-signage-map-card.is-interactive{cursor:pointer !important;box-shadow:0 8px 18px rgba(0,0,0,.16) !important;}', '.jrs-signage-map-card.is-interactive:active{transform:scale(.992) !important;}', '.jrs-signage-map-stage{position:relative !important;width:100% !important;aspect-ratio:980/453 !important;}', '.jrs-signage-map-overlay{position:absolute !important;inset:0 !important;display:flex !important;align-items:center !important;justify-content:center !important;padding:clamp(18px,3vw,40px) !important;background:rgba(0,22,34,.45) !important;backdrop-filter:blur(5px) !important;-webkit-backdrop-filter:blur(5px) !important;opacity:0 !important;pointer-events:none !important;transition:opacity .28s ease !important;z-index:10030 !important;--jrs-map-pan-x:0px !important;--jrs-map-pan-y:18px !important;--jrs-map-scale:.98 !important;}', '.jrs-signage-map-overlay.is-visible{opacity:1 !important;pointer-events:auto !important;}', '.jrs-signage-map-overlay.is-measuring{opacity:0 !important;pointer-events:none !important;background:transparent !important;backdrop-filter:none !important;-webkit-backdrop-filter:none !important;}', '.jrs-signage-map-overlay-panel{width:min(76vw,1160px) !important;max-width:100% !important;opacity:.2 !important;transform-origin:center center !important;transform:translate3d(var(--jrs-map-pan-x,0px),var(--jrs-map-pan-y,18px),0) scale(var(--jrs-map-scale,.98)) !important;transition:transform .32s cubic-bezier(.2,.72,.16,1),opacity .26s ease !important;will-change:transform,opacity !important;}', '.jrs-signage-map-overlay.is-visible .jrs-signage-map-overlay-panel{opacity:1 !important;transform:translate3d(0,0,0) scale(1) !important;}', '.jrs-signage-map-overlay.is-measuring .jrs-signage-map-overlay-panel{opacity:1 !important;transform:translate3d(0,0,0) scale(1) !important;transition:none !important;}', '.jrs-signage-map-overlay .jrs-signage-map-card{padding:clamp(8px,1vw,14px) !important;border-width:3px !important;border-radius:10px !important;box-shadow:0 18px 40px rgba(0,0,0,.28) !important;}', '.jrs-signage-left .jrs-signage-map-card{flex:0 0 auto !important;align-self:stretch !important;background:#f8fcfe !important;border:1px solid rgba(0,128,176,.26) !important;border-radius:12px !important;padding:clamp(8px,.9vw,12px) !important;box-shadow:0 2px 8px rgba(0,74,111,.08) !important;}', '.jrs-signage-left .jrs-signage-map-card.is-interactive{box-shadow:0 2px 8px rgba(0,74,111,.10) !important;}', '.jrs-signage-left .jrs-signage-map-card.is-interactive:hover{transform:translateY(-1px) !important;}', '.jrs-signage-map-stage img{position:absolute !important;display:block !important;max-width:none !important;height:auto !important;}', '.jrs-signage-map-base,.jrs-signage-map-station{left:0 !important;top:0 !important;width:100% !important;height:auto !important;}', '.jrs-signage-line{filter:drop-shadow(0 1px 2px rgba(0,90,122,.18)) !important;animation:jrsSignagePulse 1.2s ease-in-out infinite !important;}', '.jrs-signage-summary{margin-top:0 !important;flex:1 1 auto !important;display:flex !important;flex-direction:column !important;background:#eef8fc !important;color:#123443 !important;border:1px solid rgba(0,121,167,.24) !important;border-radius:12px !important;overflow-y:auto !important;overflow-x:hidden !important;-webkit-overflow-scrolling:touch !important;overscroll-behavior:contain !important;min-height:0 !important;box-shadow:0 2px 8px rgba(0,74,111,.08) !important;}', '.jrs-signage-status{flex:0 0 auto !important;background:#0d9acc !important;color:#fff !important;padding:clamp(9px,.95vw,13px) !important;border-bottom:1px solid rgba(255,255,255,.16) !important;}', '.jrs-signage-status-meta{display:flex !important;align-items:center !important;justify-content:space-between !important;gap:10px !important;margin-bottom:8px !important;}', '.jrs-signage-status-label{font-size:clamp(10px,.85vw,13px) !important;font-weight:900 !important;letter-spacing:.12em !important;opacity:.92 !important;margin-bottom:0 !important;}', '.jrs-signage-status-line-badges{display:flex !important;align-items:center !important;justify-content:flex-end !important;gap:4px !important;flex-wrap:wrap !important;}', '.jrs-signage-status-line-badge{display:inline-flex !important;align-items:center !important;justify-content:center !important;min-width:clamp(18px,1.4vw,24px) !important;height:clamp(18px,1.4vw,24px) !important;padding:0 5px !important;border-radius:999px !important;border:1px solid rgba(255,255,255,.46) !important;font-size:clamp(10px,.82vw,13px) !important;font-weight:900 !important;line-height:1 !important;}', '.jrs-signage-status-text{font-size:clamp(20px,2vw,33px) !important;font-weight:950 !important;line-height:1.08 !important;}', '.jrs-signage-status-alert{display:flex !important;align-items:center !important;gap:clamp(5px,.65vw,9px) !important;background:rgba(255,255,255,.95) !important;color:#d85b23 !important;border:1px solid rgba(216,91,35,.18) !important;border-radius:10px !important;padding:clamp(7px,.8vw,10px) !important;line-height:1.22 !important;}', '.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-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;}', '.jrs-signage-side-item.is-active{background:#0d9acc !important;color:#fff !important;border-color:#0b759d !important;box-shadow:none !important;}', '.jrs-signage-footnote{color:rgba(255,255,255,.94) !important;font-size:clamp(9px,.75vw,12px) !important;line-height:1.4 !important;padding:8px 10px !important;border-radius:8px !important;background:rgba(7,111,153,.22) !important;border:1px solid rgba(255,255,255,.10) !important;}', '.jrs-signage-right{min-width:0 !important;background:#fff !important;color:#111 !important;border:2px solid #000 !important;border-radius:5px !important;display:flex !important;flex-direction:column !important;overflow:hidden !important;}', '.jrs-signage-heading{background:#0099CC !important;color:#fff !important;border-bottom:2px solid #000 !important;padding:2px clamp(8px,1vw,14px) !important;display:grid !important;grid-template-columns:minmax(0,1fr) auto !important;align-items:center !important;gap:10px !important;height:clamp(26px,2.8vw,42px) !important;min-height:0 !important;max-height:clamp(26px,2.8vw,42px) !important;overflow:hidden !important;}', '.jrs-signage-heading-main{min-width:0 !important;overflow:hidden !important;height:100% !important;display:flex !important;align-items:center !important;}', '.jrs-signage-heading h1{margin:0 !important;color:#fff !important;font-size:clamp(16px,1.72vw,30px) !important;line-height:1 !important;font-weight:950 !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;will-change:transform !important;max-height:1em !important;}', '.jrs-signage-heading small{display:none !important;}', '.jrs-signage-updated{color:#fff !important;font-size:clamp(8px,.72vw,12px) !important;font-weight:800 !important;text-align:right !important;white-space:nowrap !important;line-height:1 !important;align-self:center !important;}', '.jrs-signage-tabs{display:flex !important;gap:5px !important;align-items:center !important;padding:1px clamp(8px,1vw,14px) !important;background:#f8f8fc !important;border-bottom:1px solid #e4e4e4 !important;overflow-x:auto !important;-webkit-overflow-scrolling:touch !important;}', '.jrs-signage-tab{flex:0 0 auto !important;min-width:clamp(26px,2vw,32px) !important;height:clamp(18px,1.55vw,25px) !important;border-radius:999px !important;border:2px solid #0099CC !important;background:#fff !important;color:#0099CC !important;font-size:clamp(10px,.86vw,14px) !important;font-weight:900 !important;cursor:pointer !important;}', '.jrs-signage-tab.is-active{background:#0099CC !important;color:#fff !important;border-color:#000 !important;}', '.jrs-signage-info-viewport{flex:1 !important;overflow-y:auto !important;padding:0 clamp(12px,1.55vw,24px) clamp(10px,1.25vw,18px) !important;-webkit-overflow-scrolling:touch !important;touch-action:pan-y !important;overscroll-behavior:contain !important;}', '.jrs-signage-info-viewport > :first-child{margin-top:0 !important;}', '.jrs-signage-midasi{position:relative !important;margin:clamp(12px,1.25vw,20px) 0 clamp(6px,.75vw,10px) !important;padding:clamp(6px,.7vw,10px) 0 clamp(6px,.7vw,10px) clamp(16px,1.45vw,24px) !important;border-bottom:3px solid #0099CC !important;color:#111 !important;font-size:clamp(22px,2.25vw,38px) !important;font-weight:950 !important;line-height:1.15 !important;}', '.jrs-signage-midasi:first-child,.jrs-signage-midasi.is-first-content{margin-top:0 !important;padding-top:0 !important;}', '.jrs-signage-body-line:first-child,.jrs-signage-body-line.is-first-content{margin-top:0 !important;padding-top:0 !important;line-height:1.32 !important;}', '.jrs-signage-midasi:before{content:"" !important;position:absolute !important;left:0 !important;top:50% !important;width:6px !important;height:calc(100% - 14px) !important;min-height:28px !important;max-height:58px !important;transform:translateY(-50%) !important;background:#0099CC !important;border-radius:999px !important;}', '.jrs-signage-body-line{color:#111 !important;font-size:clamp(22px,2.25vw,36px) !important;line-height:1.48 !important;font-weight:700 !important;margin:0 0 clamp(7px,.7vw,11px) !important;}', '.jrs-signage-empty{display:flex !important;align-items:center !important;justify-content:center !important;min-height:55vh !important;color:#555 !important;font-size:clamp(26px,3vw,46px) !important;font-weight:900 !important;text-align:center !important;}', '.jrs-signage-side-item.is-empty{background:#0d9acc !important;color:#fff !important;border-color:#0b759d !important;box-shadow:none !important;}', '.jrs-signage-status-ok{display:flex !important;align-items:center !important;gap:clamp(8px,.85vw,12px) !important;margin-top:clamp(9px,1vw,14px) !important;padding:clamp(9px,1vw,13px) !important;border-radius:10px !important;background:#ffffff !important;border:1px solid rgba(0,129,176,.16) !important;}', '.jrs-signage-status-ok-icon{width:clamp(28px,2.05vw,36px) !important;height:clamp(28px,2.05vw,36px) !important;border-radius:999px !important;background:#35b56f !important;border:1px solid rgba(0,0,0,.04) !important;display:flex !important;align-items:center !important;justify-content:center !important;flex:0 0 auto !important;font-size:clamp(15px,1.1vw,19px) !important;font-weight:950 !important;line-height:1 !important;color:#fff !important;}', '.jrs-signage-status-ok-copy{display:flex !important;flex-direction:column !important;gap:2px !important;min-width:0 !important;}', '.jrs-signage-status-ok-title{font-size:clamp(14px,1.2vw,19px) !important;font-weight:950 !important;line-height:1.15 !important;color:#164151 !important;}', '.jrs-signage-status-ok-sub{font-size:clamp(11px,.9vw,14px) !important;font-weight:700 !important;line-height:1.3 !important;color:#4e6f79 !important;}', '.jrs-signage-empty-state{min-height:100% !important;display:flex !important;align-items:center !important;justify-content:center !important;padding:clamp(12px,1.6vw,22px) !important;}', '.jrs-signage-empty-state-card{width:min(860px,100%) !important;max-height:100% !important;margin:auto !important;padding:clamp(16px,2vw,26px) clamp(18px,2.4vw,32px) !important;border-radius:12px !important;background:#f7fbfd !important;border:1px solid rgba(0,153,203,.16) !important;box-shadow:0 2px 8px rgba(0,86,77,.06) !important;text-align:center !important;overflow:auto !important;}', '.jrs-signage-empty-state-route{position:relative !important;width:min(100%,520px) !important;height:clamp(46px,4.5vw,64px) !important;margin:0 auto clamp(10px,1vw,14px) !important;}', '.jrs-signage-empty-state-route-line{position:absolute !important;left:7% !important;right:7% !important;top:50% !important;height:6px !important;border-radius:999px !important;transform:translateY(-50%) !important;background:#44b8df !important;}', '.jrs-signage-empty-state-route-stop{position:absolute !important;top:50% !important;width:14px !important;height:14px !important;margin-top:-7px !important;border-radius:999px !important;background:#fff !important;border:4px solid #1298ca !important;}', '.jrs-signage-empty-state-route-stop.is-start{left:9% !important;}', '.jrs-signage-empty-state-route-stop.is-mid{left:31% !important;}', '.jrs-signage-empty-state-route-stop.is-end{right:9% !important;}', '.jrs-signage-empty-state-route-train{position:absolute !important;left:50% !important;top:50% !important;width:clamp(60px,6vw,84px) !important;height:clamp(30px,2.9vw,42px) !important;border-radius:10px !important;transform:translate(-50%,-50%) !important;background:#0d9acc !important;border:2px solid rgba(255,255,255,.88) !important;}', '.jrs-signage-empty-state-route-train:before{content:"" !important;position:absolute !important;left:18% !important;right:18% !important;top:28% !important;height:28% !important;border-radius:999px !important;background:rgba(255,255,255,.9) !important;}', '.jrs-signage-empty-state-route-train:after{content:"" !important;position:absolute !important;left:22% !important;bottom:16% !important;width:56% !important;height:5px !important;border-radius:999px !important;background:rgba(255,255,255,.72) !important;}', '.jrs-signage-empty-state-eyebrow{display:inline-flex !important;align-items:center !important;justify-content:center !important;margin:0 auto clamp(8px,.8vw,12px) !important;padding:5px 11px !important;border-radius:8px !important;background:#e7f5fb !important;color:#007fae !important;font-size:clamp(11px,.82vw,13px) !important;font-weight:900 !important;letter-spacing:.08em !important;}', '.jrs-signage-empty-state-title{margin:0 0 clamp(8px,.9vw,12px) !important;color:#173843 !important;font-size:clamp(28px,2.7vw,42px) !important;font-weight:950 !important;line-height:1.04 !important;}', '.jrs-signage-empty-state-lead{margin:0 auto clamp(6px,.75vw,10px) !important;max-width:20em !important;color:#2d5560 !important;font-size:clamp(17px,1.45vw,23px) !important;font-weight:800 !important;line-height:1.34 !important;}', '.jrs-signage-empty-state-note{margin:0 auto !important;max-width:24em !important;color:#52717a !important;font-size:clamp(12px,.92vw,15px) !important;font-weight:700 !important;line-height:1.36 !important;}', '.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(''); } function postMessage(payload) { if (!window.ReactNativeWebView) return; payload.type = 'operationInfoCapture'; window.ReactNativeWebView.postMessage(JSON.stringify(payload)); } function parseDetailBlocks(html) { var temp = document.createElement('div'); temp.innerHTML = html; qa('.jrs-subcapture-wrap', temp).forEach(function(element) { if (element.parentNode) element.parentNode.removeChild(element); }); qa('.midasi', temp).forEach(function(element) { var label = strip(element.textContent); element.replaceWith(document.createTextNode('\n[[MIDASI:' + label + ']]\n')); }); qa('br', temp).forEach(function(element) { element.replaceWith(document.createTextNode('\n')); }); return String(temp.textContent || '') .split(/\n+/) .map(function(line) { return strip(line); }) .filter(function(line) { return !!line; }) .map(function(line) { var match = line.match(/^\[\[MIDASI:(.*)\]\]$/); if (match) { return { type: 'badge', text: strip(match[1]) }; } return { type: 'body', text: line }; }); } function wrapText(ctx, text, maxWidth) { var value = strip(text); if (!value) return []; var words = value.split(/\s+/); var lines = []; var current = ''; words.forEach(function(word) { var candidate = current ? current + ' ' + word : word; if (!current || ctx.measureText(candidate).width <= maxWidth) { current = candidate; } else { lines.push(current); current = word; } }); if (current) lines.push(current); return lines; } function loadImage(src) { return new Promise(function(resolve, reject) { var image = new Image(); image.onload = function() { resolve(image); }; image.onerror = reject; image.src = src; }); } function resolveAsset(path) { try { return new URL(path, window.location.href).href; } catch (e) { return path; } } async function buildMapImage(targetWidth) { var base = await loadImage(resolveAsset('img/map/base.png')); var baseWidth = 980; var scale = targetWidth / baseWidth; var targetHeight = Math.round((base.naturalHeight || base.height) * scale); var mapCanvas = document.createElement('canvas'); var mapCtx = mapCanvas.getContext('2d'); if (!mapCtx) return null; mapCanvas.width = targetWidth; mapCanvas.height = targetHeight; mapCtx.fillStyle = '#ffffff'; mapCtx.fillRect(0, 0, mapCanvas.width, mapCanvas.height); mapCtx.drawImage(base, 0, 0, targetWidth, targetHeight); var layers = [ { selector: '.ml_seto', src: 'img/map/seto.png', x: 585, y: 2, w: 143, h: 77 }, { selector: '.ml_yosan1', src: 'img/map/yosan1.png', x: 183, y: 50, w: 419, h: 71 }, { selector: '.ml_yosan2', src: 'img/map/yosan2.png', x: 78, y: 83, w: 121, h: 242 }, { selector: '.ml_dosan1', src: 'img/map/dosan1.png', x: 468, y: 64, w: 133, h: 265 }, { selector: '.ml_dosan2', src: 'img/map/dosan2.png', x: 259, y: 314, w: 224, h: 100 }, { selector: '.ml_kotoku', src: 'img/map/kotoku.png', x: 715, y: 64, w: 167, h: 179 }, { selector: '.ml_mugi', src: 'img/map/mugi.png', x: 767, y: 230, w: 115, h: 196 }, { selector: '.ml_tokushima', src: 'img/map/tokushima.png', x: 601, y: 166, w: 274, h: 78 }, { selector: '.ml_naruto', src: 'img/map/naruto.png', x: 867, y: 167, w: 56, h: 77 }, { selector: '.ml_yodo', src: 'img/map/yodo.png', x: 94, y: 296, w: 180, h: 125 } ]; for (var i = 0; i < layers.length; i += 1) { var layer = layers[i]; var element = q(layer.selector); var display = element ? window.getComputedStyle(element).display : 'none'; if (display === 'none') continue; try { var layerImage = await loadImage(resolveAsset(layer.src)); mapCtx.drawImage( layerImage, Math.round(layer.x * scale), Math.round(layer.y * scale), Math.round(layer.w * scale), Math.round(layer.h * scale) ); } catch (e) {} } try { var station = await loadImage(resolveAsset('img/map/station.png')); mapCtx.drawImage(station, 0, 0, targetWidth, targetHeight); } catch (e) {} return mapCanvas; } function wrapText(ctx, text, maxWidth) { var value = strip(text); if (!value) return []; var chars = Array.from(value); var lines = []; var current = ''; chars.forEach(function(char) { var candidate = current + char; if (!current || ctx.measureText(candidate).width <= maxWidth) { current = candidate; } else { lines.push(current); current = char; } }); if (current) lines.push(current); return lines; } function appendCaptureItemLayout(layout, ctx, item, startY, options) { var headerX = options && options.x != null ? options.x : options.paddingX; var headerWidth = options && options.width != null ? options.width : options.contentWidth; var headerTextWidth = headerWidth - 48; var currentY = startY; ctx.font = "700 40px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; var titleLines = wrapText(ctx, item.title || '', headerTextWidth); ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; var subTitleLines = strip(item.subTitle) ? wrapText(ctx, item.subTitle, headerTextWidth) : []; var headerHeight = Math.max(112, 34 + titleLines.length * 52 + subTitleLines.length * 44 + 24); layout.push({ type: 'header', x: headerX, y: currentY, width: headerWidth, height: headerHeight, titleLines: titleLines, subTitleLines: subTitleLines }); currentY += headerHeight; if (options && options.showDetailBox === false) { return currentY + (options.sectionGap || 34); } var boxX = headerX; var boxY = currentY; var boxWidth = headerWidth; var boxPaddingX = options && options.innerPaddingX != null ? options.innerPaddingX : 30; var boxContentWidth = boxWidth - boxPaddingX * 2; currentY += 30; if (strip(item.updatedAt)) { ctx.font = "500 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; var updatedLines = wrapText(ctx, item.updatedAt, boxContentWidth); layout.push({ type: 'updated', lines: updatedLines, y: currentY, lineHeight: 40, boxX: boxX, boxWidth: boxWidth, paddingX: boxPaddingX }); currentY += updatedLines.length * 40 + 28; } (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') { 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); }); } 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 }); 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 || 26); } function drawCaptureLayout(ctx, layout, canvasWidth, contentWidth, paddingX) { layout.forEach(function(item) { if (item.type === 'detailBox') { ctx.strokeStyle = '#cccccc'; ctx.lineWidth = 2; ctx.strokeRect(item.x, item.y, item.width, item.height); } }); layout.forEach(function(item) { if (item.type === 'map') { ctx.strokeStyle = '#CBE8F6'; ctx.lineWidth = 2; ctx.strokeRect(item.x, item.y, item.width, item.height); ctx.drawImage(item.canvas, item.x, item.y, item.width, item.height); return; } if (item.type === 'header') { ctx.fillStyle = '#0099CB'; ctx.fillRect(item.x, item.y, item.width, item.height); var titleLineHeight = 52; var subTitleLineHeight = 44; var textBlockHeight = item.titleLines.length * titleLineHeight + item.subTitleLines.length * subTitleLineHeight; var textTop = item.y + Math.max(22, Math.round((item.height - textBlockHeight) / 2)); var highlightTop = Math.max(item.y + 18, textTop - 8); var highlightHeight = Math.min(item.height - 36, textBlockHeight + 16); ctx.fillStyle = '#CDEBF5'; ctx.fillRect(item.x + 16, highlightTop, 4, highlightHeight); ctx.fillStyle = '#ffffff'; ctx.font = "700 40px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; var textY = textTop + 39; item.titleLines.forEach(function(line) { ctx.fillText(line, item.x + 34, textY); textY += titleLineHeight; }); ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; item.subTitleLines.forEach(function(line) { ctx.fillText(line, item.x + 34, textY - 3); textY += subTitleLineHeight; }); return; } if (item.type === 'updated') { ctx.fillStyle = '#0099CB'; ctx.font = "500 28px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; item.lines.forEach(function(line, index) { var textWidth = ctx.measureText(line).width; ctx.fillText(line, item.boxX + item.boxWidth - item.paddingX - textWidth, item.y + index * item.lineHeight); }); ctx.strokeStyle = '#d8d8d8'; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(item.boxX + item.paddingX, item.y + item.lines.length * item.lineHeight + 8); ctx.lineTo(item.boxX + item.boxWidth - item.paddingX, item.y + item.lines.length * item.lineHeight + 8); ctx.stroke(); ctx.setLineDash([]); return; } if (item.type === 'midasi') { var blockTop = item.y - 30; var blockHeight = Math.max(64, item.lines.length * item.lineHeight + 18); ctx.fillStyle = '#ffffff'; ctx.fillRect(item.boxX + item.paddingX, blockTop, item.boxWidth - item.paddingX * 2, blockHeight); ctx.fillStyle = '#0099CB'; ctx.fillRect(item.boxX + item.paddingX, blockTop + 8, 8, blockHeight - 16); ctx.strokeStyle = '#0099CB'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(item.boxX + item.paddingX, blockTop + blockHeight - 4); ctx.lineTo(item.boxX + item.boxWidth - item.paddingX, blockTop + blockHeight - 4); ctx.stroke(); ctx.fillStyle = '#222222'; ctx.font = "700 32px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; item.lines.forEach(function(line, index) { ctx.fillText(line, item.boxX + item.paddingX + 22, item.y + index * item.lineHeight); }); return; } if (item.type === 'body') { ctx.fillStyle = '#222222'; ctx.font = "400 31px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; item.lines.forEach(function(line, index) { ctx.fillText(line, item.boxX + item.paddingX, item.y + index * item.lineHeight); }); return; } if (item.type === 'notice') { var notice = 'この画像は非公式アプリのキャプチャ機能で切り取られた内容です。最新情報・詳細はJR四国公式の運行情報をご確認ください。'; ctx.fillStyle = '#666666'; ctx.font = "400 22px -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', sans-serif"; wrapText(ctx, notice, item.width).forEach(function(line, index) { ctx.fillText(line, item.x, item.y + index * 30); }); } }); } async function renderItemToImage(title, subTitle, updatedAt, detailHtml, fileNameBase) { try { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (!ctx) { postMessage({ error: true }); return; } var blocks = parseDetailBlocks(detailHtml); var sections = splitCaptureBlocksIntoSections(blocks); var useSectionColumns = sections.length > 1; var singleWidth = 1080; var paddingX = 46; var paddingY = 38; 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); } catch (e) { mapCanvas = null; } var layout = []; var y = paddingY; if (mapCanvas) { layout.push({ type: 'map', x: paddingX, y: y, width: contentWidth, height: mapCanvas.height, canvas: mapCanvas }); y += mapCanvas.height + 28; } 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; canvas.width = width; canvas.height = Math.max(720, Math.ceil(y + paddingY)); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); drawCaptureLayout(ctx, layout, width, contentWidth, paddingX); postMessage({ dataUrl: canvas.toDataURL('image/png'), fileName: fileNameBase + '.png' }); } catch (e) { postMessage({ error: true }); } } async function renderAllItemsToImage(items, statusText, fileNameBase) { try { if (!items || !items.length) { postMessage({ error: true }); return; } var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (!ctx) { postMessage({ error: true }); return; } var singleWidth = 1080; var paddingX = 46; var paddingY = 38; var singleContentWidth = singleWidth - paddingX * 2; var columnGap = 24; 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; var layout = []; var y = paddingY; var mapCanvas = null; try { mapCanvas = await buildMapImage(contentWidth); } catch (e) { mapCanvas = null; } if (mapCanvas) { layout.push({ type: 'map', x: paddingX, y: y, width: contentWidth, height: mapCanvas.height, canvas: mapCanvas }); y += mapCanvas.height + 28; } if (strip(statusText)) { y = appendCaptureItemLayout(layout, ctx, { title: statusText, subTitle: '表示中の運行情報一覧', updatedAt: '', blocks: [] }, y, { x: paddingX, width: contentWidth, showDetailBox: false, sectionGap: 24 }); } var columnHeights = []; var columnIndex; for (columnIndex = 0; columnIndex < columnCount; columnIndex += 1) { columnHeights.push(y); } items.forEach(function(item) { 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, sectionGap: 26 }); }); y = Math.max.apply(Math, columnHeights); layout.push({ type: 'notice', y: y, x: paddingX, width: contentWidth }); y += 54; canvas.width = width; canvas.height = Math.max(960, Math.ceil(y + paddingY)); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); drawCaptureLayout(ctx, layout, width, contentWidth, paddingX); postMessage({ dataUrl: canvas.toDataURL('image/png'), fileName: fileNameBase + '.png' }); } catch (e) { postMessage({ error: true }); } } function getSubsectionHtml(midasi) { var temp = document.createElement('div'); temp.appendChild(midasi.cloneNode(true)); var node = midasi.nextSibling; while (node) { if (node.nodeType === 1 && node.classList && node.classList.contains('midasi')) { break; } if (!(node.nodeType === 1 && node.classList && node.classList.contains('jrs-subcapture-wrap'))) { temp.appendChild(node.cloneNode(true)); } node = node.nextSibling; } return temp.innerHTML; } function getTitleText(heading) { return Array.prototype.slice.call(heading.childNodes) .filter(function(node) { return node.nodeType === Node.TEXT_NODE; }) .map(function(node) { return node.textContent || ''; }) .join(' '); } function installSubCaptureButtons(dd, heading, infoId) { var detailNode = q('.detail_txt', dd); if (!detailNode) return; qa('.midasi', detailNode).forEach(function(midasi, index) { var next = midasi.nextSibling; if (next && next.nodeType === 1 && next.classList && next.classList.contains('jrs-subcapture-wrap')) { return; } var wrap = document.createElement('div'); wrap.className = 'jrs-subcapture-wrap'; var link = document.createElement('a'); link.href = '#'; link.className = 'jrs-subcapture-link'; link.textContent = 'この小見出しを切り出す'; 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() ); }; wrap.appendChild(link); if (midasi.nextSibling) { midasi.parentNode.insertBefore(wrap, midasi.nextSibling); } else { midasi.parentNode.appendChild(wrap); } }); } function installPageCaptureButton() { var infoRoot = q('.delay_info'); if (!infoRoot) return; var items = getOperationItems(); var existingWrap = q('.jrs-capture-page-wrap'); if (!items.length) { if (existingWrap && existingWrap.parentNode) existingWrap.parentNode.removeChild(existingWrap); return; } if (existingWrap) return; 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); if (infoRoot.parentNode) { infoRoot.parentNode.insertBefore(wrap, infoRoot); } } function removeCaptureButtons() { removeNodes(document, '.jrs-capture-page-wrap, .jrs-capture-wrap, .jrs-subcapture-wrap'); } function installCaptureButtons() { var nativeLayout = window.__jrsNativeOperationLayout || {}; if (!nativeLayout.enableCapture) { removeCaptureButtons(); return; } installPageCaptureButton(); qa('.delay_info dd[dinfo_id]').forEach(function(dd) { var infoId = dd.getAttribute('dinfo_id'); var dt = q('.delay_info dt[dinfo_id="' + infoId + '"]'); var heading = dt ? q('h2', dt) : null; if (!infoId || !heading) { return; } var detailNode = q('.detail_txt', dd); if (!detailNode) { return; } if (!q('.jrs-capture-wrap', dd)) { var wrap = document.createElement('div'); wrap.className = 'jrs-capture-wrap'; var link = document.createElement('a'); link.href = '#'; link.className = 'jrs-capture-link'; link.textContent = 'この項目を切り出す'; 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() ); }; wrap.appendChild(link); dd.insertBefore(wrap, dd.firstChild); } installSubCaptureButtons(dd, heading, infoId); }); } 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); return width > height && width / height >= 1.35; } function getSignageState() { if (!window.__jrSignageState) { window.__jrSignageState = { index: 0, pausedUntil: 0, scrollTimer: null, dwellTimer: null, userInteractedAt: 0, isAutoScrolling: false, viewportMisses: 0 }; } return window.__jrSignageState; } function clearSignageTimers() { var state = getSignageState(); if (state.scrollTimer) window.clearInterval(state.scrollTimer); if (state.dwellTimer) window.clearTimeout(state.dwellTimer); state.scrollTimer = null; state.dwellTimer = null; } function removeSignageMode() { clearSignageTimers(); var root = q('#jrs-signage-root'); if (root && root.parentNode) { root.parentNode.removeChild(root); } document.documentElement.classList.remove('jrs-signage-active'); } function getOperationStatus(items) { var status = q('.delay_status'); var statusText = getStatusText(status); var statusIcon = status ? q('img', status) : null; var iconSrc = statusIcon ? (statusIcon.currentSrc || statusIcon.src || statusIcon.getAttribute('src') || '') : ''; return { text: statusText || (items && items.length ? '運行情報あり' : '運行情報なし'), iconSrc: iconSrc, hasWarning: !!statusText }; } function getOperationItems() { var items = []; qa('.delay_info dd[dinfo_id]').forEach(function(dd) { var infoId = dd.getAttribute('dinfo_id'); var dt = q('.delay_info dt[dinfo_id="' + infoId + '"]'); var heading = dt ? q('h2', dt) : null; var detailNode = q('.detail_txt', dd); if (!infoId || !heading || !detailNode) return; items.push({ infoId: infoId, title: getTitleText(heading) || '運行情報', subTitle: strip((q('.delay_subttl', heading) || {}).textContent), updatedAt: strip((q('.upd_time', dd) || {}).textContent), blocks: parseDetailBlocks(detailNode.innerHTML) }); }); return items; } function appendText(parent, tagName, className, text) { var element = document.createElement(tagName); if (className) element.className = className; element.textContent = text || ''; parent.appendChild(element); return element; } function appendSteadyOperationState(viewport) { var wrap = document.createElement('div'); wrap.className = 'jrs-signage-empty-state'; var card = document.createElement('div'); card.className = 'jrs-signage-empty-state-card'; var route = document.createElement('div'); route.className = 'jrs-signage-empty-state-route'; appendText(route, 'div', 'jrs-signage-empty-state-route-line', ''); appendText(route, 'div', 'jrs-signage-empty-state-route-stop is-start', ''); appendText(route, 'div', 'jrs-signage-empty-state-route-stop is-mid', ''); appendText(route, 'div', 'jrs-signage-empty-state-route-stop is-end', ''); appendText(route, 'div', 'jrs-signage-empty-state-route-train', ''); card.appendChild(route); appendText(card, 'div', 'jrs-signage-empty-state-eyebrow', 'NORMAL OPERATION'); appendText(card, 'h2', 'jrs-signage-empty-state-title', '平常運転'); appendText(card, 'p', 'jrs-signage-empty-state-lead', '現在、JR四国管内に大きな運行情報はなく、列車はおおむね定刻どおり運転しています。'); appendText(card, 'p', 'jrs-signage-empty-state-note', '新しい遅延・運休などのお知らせが入ると、この画面に内容を表示します。'); wrap.appendChild(card); viewport.appendChild(wrap); return wrap; } function pauseSignageAutoScroll(durationMs) { var state = getSignageState(); state.pausedUntil = Date.now() + (durationMs || 7000); state.userInteractedAt = Date.now(); } function setSignageIndex(index, manual) { var state = getSignageState(); var root = q('#jrs-signage-root'); if (!root) return; var count = Number(root.getAttribute('data-count') || 0); if (!count) return; state.index = ((index % count) + count) % count; if (manual) pauseSignageAutoScroll(9000); renderSignageActiveItem(root); } function populateSignageMapStage(stage) { var base = document.createElement('img'); base.className = 'jrs-signage-map-base'; base.src = resolveAsset('img/map/base.png'); base.alt = ''; stage.appendChild(base); [ { selector: '.ml_seto', src: 'img/map/seto.png', x: 585, y: 2, w: 143, h: 77 }, { selector: '.ml_yosan1', src: 'img/map/yosan1.png', x: 183, y: 50, w: 419, h: 71 }, { selector: '.ml_yosan2', src: 'img/map/yosan2.png', x: 78, y: 83, w: 121, h: 242 }, { selector: '.ml_dosan1', src: 'img/map/dosan1.png', x: 468, y: 64, w: 133, h: 265 }, { selector: '.ml_dosan2', src: 'img/map/dosan2.png', x: 259, y: 314, w: 224, h: 100 }, { selector: '.ml_kotoku', src: 'img/map/kotoku.png', x: 715, y: 64, w: 167, h: 179 }, { selector: '.ml_mugi', src: 'img/map/mugi.png', x: 767, y: 230, w: 115, h: 196 }, { selector: '.ml_tokushima', src: 'img/map/tokushima.png', x: 601, y: 166, w: 274, h: 78 }, { selector: '.ml_naruto', src: 'img/map/naruto.png', x: 867, y: 167, w: 56, h: 77 }, { selector: '.ml_yodo', src: 'img/map/yodo.png', x: 94, y: 296, w: 180, h: 125 } ].forEach(function(layer) { var source = q(layer.selector); var display = source ? window.getComputedStyle(source).display : 'none'; if (display === 'none') return; var image = document.createElement('img'); image.className = 'jrs-signage-line'; image.src = resolveAsset(layer.src); image.alt = ''; image.style.left = (layer.x / 980 * 100) + '%'; image.style.top = (layer.y / 453 * 100) + '%'; image.style.width = (layer.w / 980 * 100) + '%'; stage.appendChild(image); }); var station = document.createElement('img'); station.className = 'jrs-signage-map-station'; station.src = resolveAsset('img/map/station.png'); station.alt = ''; stage.appendChild(station); } function appendSignageMap(parent) { var card = document.createElement('div'); card.className = 'jrs-signage-map-card'; var stage = document.createElement('div'); stage.className = 'jrs-signage-map-stage'; card.appendChild(stage); populateSignageMapStage(stage); parent.appendChild(card); return card; } function syncSignageMapOverlayMotion(root) { 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); if (!overlay || !sourceCard || !panel) return; overlay.classList.add('is-measuring'); var sourceRect = sourceCard.getBoundingClientRect(); var panelRect = panel.getBoundingClientRect(); overlay.classList.remove('is-measuring'); if (!sourceRect.width || !sourceRect.height || !panelRect.width || !panelRect.height) { overlay.style.setProperty('--jrs-map-pan-x', '0px'); overlay.style.setProperty('--jrs-map-pan-y', '18px'); overlay.style.setProperty('--jrs-map-scale', '.98'); return; } var sourceCenterX = sourceRect.left + sourceRect.width / 2; var sourceCenterY = sourceRect.top + sourceRect.height / 2; var panelCenterX = panelRect.left + panelRect.width / 2; var panelCenterY = panelRect.top + panelRect.height / 2; var translateX = sourceCenterX - panelCenterX; var translateY = sourceCenterY - panelCenterY; var scale = Math.min(sourceRect.width / panelRect.width, sourceRect.height / panelRect.height); if (!isFinite(scale) || scale <= 0) scale = 0.98; overlay.style.setProperty('--jrs-map-pan-x', translateX.toFixed(2) + 'px'); overlay.style.setProperty('--jrs-map-pan-y', translateY.toFixed(2) + 'px'); overlay.style.setProperty('--jrs-map-scale', Math.max(0.18, Math.min(scale, 1)).toFixed(4)); } function closeSignageMapOverlay(root) { var overlay = q('.jrs-signage-map-overlay', root); if (!overlay || !overlay.classList.contains('is-visible')) return; syncSignageMapOverlayMotion(root); window.requestAnimationFrame(function() { overlay.classList.remove('is-visible'); }); } function openSignageMapOverlay(root) { var overlay = q('.jrs-signage-map-overlay', root); if (!overlay) return; syncSignageMapOverlayMotion(root); overlay.classList.remove('is-visible'); overlay.offsetWidth; window.requestAnimationFrame(function() { overlay.classList.add('is-visible'); }); } function createSignageMapOverlay(root) { var overlay = document.createElement('div'); overlay.className = 'jrs-signage-map-overlay'; var panel = document.createElement('div'); panel.className = 'jrs-signage-map-overlay-panel'; var card = appendSignageMap(panel); panel.appendChild(card); overlay.appendChild(panel); overlay.addEventListener('click', function(event) { if (event.target === overlay) { closeSignageMapOverlay(root); } }); panel.addEventListener('click', function(event) { event.stopPropagation(); }); root.appendChild(overlay); return overlay; } 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'); } function buildSignageSignature(items, statusInfo) { return [ window.innerWidth, window.innerHeight, statusInfo ? statusInfo.text : '', statusInfo ? statusInfo.iconSrc : '', statusInfo && statusInfo.hasWarning ? 'warning' : '', getNativeLineBadges().map(function(entry) { return entry.code + ':' + entry.color; }).join(','), strip((q('.delay_status') || {}).textContent), items.map(function(item) { return [item.infoId, item.title, item.subTitle, item.updatedAt, item.blocks.map(function(block) { return block.type + ':' + block.text; }).join('|')].join('/'); }).join('//') ].join('::'); } function getSignageItems(root) { try { return JSON.parse(root.getAttribute('data-items') || '[]'); } catch (e) { return []; } } function renderSignageActiveItem(root) { var items = getSignageItems(root); var state = getSignageState(); if (state.index >= items.length) state.index = 0; var item = items[state.index]; var titleNode = q('.jrs-signage-current-title', root); var subTitleNode = q('.jrs-signage-current-subtitle', root); var updatedNode = q('.jrs-signage-updated', root); var viewport = q('.jrs-signage-info-viewport', root); var tabNodes = qa('.jrs-signage-tab', root); var sideNodes = qa('.jrs-signage-side-item', root); tabNodes.forEach(function(node, index) { node.classList.toggle('is-active', index === state.index); }); sideNodes.forEach(function(node, index) { node.classList.toggle('is-active', index === state.index); }); if (!item) { var isSteadyOperation = root.getAttribute('data-steady-operation') === 'true'; if (titleNode) titleNode.textContent = isSteadyOperation ? '平常運転' : '運行情報'; if (subTitleNode) subTitleNode.textContent = ''; if (updatedNode) updatedNode.textContent = ''; if (viewport) { viewport.innerHTML = ''; if (isSteadyOperation) { appendSteadyOperationState(viewport); } else { appendText(viewport, 'div', 'jrs-signage-empty', '現在表示できる運行情報がありません'); } } startSignageAutoScroll(root); return; } if (titleNode) { titleNode.textContent = item.subTitle || item.title || '運行情報'; titleNode.classList.remove('is-overflow'); titleNode.style.removeProperty('--jrs-title-overflow'); window.requestAnimationFrame(function() { var parent = titleNode.parentNode; var overflow = parent ? Math.max(0, titleNode.scrollWidth - parent.clientWidth + 18) : 0; if (overflow > 48) { titleNode.style.setProperty('--jrs-title-overflow', overflow + 'px'); titleNode.classList.add('is-overflow'); } }); } if (subTitleNode) subTitleNode.textContent = ''; if (updatedNode) updatedNode.textContent = item.updatedAt || ''; if (viewport) { viewport.innerHTML = ''; item.blocks.forEach(function(block, index) { var className = block.type === 'badge' ? 'jrs-signage-midasi' : 'jrs-signage-body-line'; if (index === 0) className += ' is-first-content'; appendText(viewport, 'div', className, block.text); }); if (!item.blocks.length) appendText(viewport, 'div', 'jrs-signage-empty', '本文情報がありません'); viewport.scrollTop = 0; } startSignageAutoScroll(root); } function startSignageAutoScroll(root) { clearSignageTimers(); var state = getSignageState(); var viewport = q('.jrs-signage-info-viewport', root); var count = Number(root.getAttribute('data-count') || 0); if (!viewport || !count) return; function scheduleNext(delay) { if (state.dwellTimer) window.clearTimeout(state.dwellTimer); state.dwellTimer = window.setTimeout(function() { if (Date.now() < state.pausedUntil) { scheduleNext(Math.max(120, state.pausedUntil - Date.now())); return; } setSignageIndex(state.index + 1, false); }, delay); } window.requestAnimationFrame(function() { var maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight); if (maxScroll < 48) { scheduleNext(2600); return; } state.scrollTimer = window.setInterval(function() { if (Date.now() < state.pausedUntil) return; var remaining = viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop; if (remaining <= 2) { window.clearInterval(state.scrollTimer); state.scrollTimer = null; scheduleNext(900); return; } state.isAutoScrolling = true; viewport.scrollTop = Math.min(viewport.scrollTop + 1.6, viewport.scrollHeight - viewport.clientHeight); window.setTimeout(function() { state.isAutoScrolling = false; }, 80); }, 28); }); } function attachSignageScrollPause(root) { var viewport = q('.jrs-signage-info-viewport', root); if (!viewport) return; ['touchstart', 'pointerdown', 'wheel'].forEach(function(eventName) { viewport.addEventListener(eventName, function() { pauseSignageAutoScroll(1200); }, { passive: true }); }); } 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(); return; } state.viewportMisses = 0; var items = getOperationItems(); var statusInfo = getOperationStatus(items); var signature = buildSignageSignature(items, statusInfo); var existing = q('#jrs-signage-root'); if (existing && existing.getAttribute('data-signature') === signature) { applySignageNativeInsets(existing); document.documentElement.classList.add('jrs-signage-active'); var existingState = getSignageState(); if (!existingState.scrollTimer && !existingState.dwellTimer) startSignageAutoScroll(existing); return; } if (existing && existing.parentNode) existing.parentNode.removeChild(existing); if (state.index >= items.length) state.index = 0; clearSignageTimers(); var root = document.createElement('div'); root.id = 'jrs-signage-root'; applySignageNativeInsets(root); root.setAttribute('data-signature', signature); root.setAttribute('data-count', String(items.length)); root.setAttribute('data-items', JSON.stringify(items)); root.setAttribute('data-steady-operation', !items.length && !(statusInfo && statusInfo.hasWarning) ? 'true' : 'false'); var shell = document.createElement('div'); shell.className = 'jrs-signage-shell'; root.appendChild(shell); var left = document.createElement('section'); left.className = 'jrs-signage-left'; shell.appendChild(left); var brand = document.createElement('div'); brand.className = 'jrs-signage-brand'; appendText(brand, 'div', '', '運行情報'); appendText(brand, 'div', 'jrs-signage-clock', new Date().toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })); 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); }); var summary = document.createElement('div'); summary.className = 'jrs-signage-summary'; var status = document.createElement('div'); status.className = 'jrs-signage-status'; var statusMeta = document.createElement('div'); statusMeta.className = 'jrs-signage-status-meta'; appendText(statusMeta, 'div', 'jrs-signage-status-label', 'STATUS'); var lineBadgeRail = document.createElement('div'); lineBadgeRail.className = 'jrs-signage-status-line-badges'; getNativeLineBadges().forEach(function(entry) { var badge = appendText(lineBadgeRail, 'span', 'jrs-signage-status-line-badge', entry.code); badge.style.backgroundColor = entry.color; badge.style.color = getContrastTextColor(entry.color); }); statusMeta.appendChild(lineBadgeRail); status.appendChild(statusMeta); var isSteadyOperation = !items.length && !(statusInfo && statusInfo.hasWarning); var primaryStatusText = (items.length || (statusInfo && statusInfo.hasWarning)) ? '運行に関する情報があります' : '現在、平常どおり運転しています'; appendText(status, 'div', 'jrs-signage-status-text', primaryStatusText); if (!isSteadyOperation && statusInfo && statusInfo.hasWarning && statusInfo.text) { var statusAlert = document.createElement('div'); statusAlert.className = 'jrs-signage-status-alert'; if (statusInfo.iconSrc) { var statusIcon = document.createElement('img'); statusIcon.className = 'jrs-signage-status-alert-icon'; statusIcon.src = statusInfo.iconSrc; statusIcon.alt = ''; statusAlert.appendChild(statusIcon); } else { appendText(statusAlert, 'span', 'jrs-signage-status-alert-symbol', ''); } appendText(statusAlert, 'div', 'jrs-signage-status-alert-text', statusInfo.text); status.appendChild(statusAlert); } summary.appendChild(status); var shouldShowSideList = !!items.length || !!(statusInfo && statusInfo.hasWarning); if (shouldShowSideList) { var sideList = document.createElement('div'); sideList.className = 'jrs-signage-side-list'; if (!items.length) { appendText(sideList, 'div', 'jrs-signage-side-item is-active', '表示できる項目がありません'); } items.forEach(function(item, index) { var button = appendText( sideList, 'button', 'jrs-signage-side-item', item.subTitle || item.title || ('項目 ' + (index + 1)) ); button.type = 'button'; button.onclick = function() { setSignageIndex(index, true); }; }); summary.appendChild(sideList); } left.appendChild(summary); appendText(left, 'div', 'jrs-signage-footnote', 'この表示は非公式アプリ内で公式ページの内容を見やすく再配置したものです。詳細・最新情報はJR四国公式の運行情報をご確認ください。'); var right = document.createElement('section'); right.className = 'jrs-signage-right'; shell.appendChild(right); var heading = document.createElement('div'); heading.className = 'jrs-signage-heading'; var headingMain = document.createElement('div'); headingMain.className = 'jrs-signage-heading-main'; appendText(headingMain, 'h1', 'jrs-signage-current-title', '運行情報'); appendText(headingMain, 'small', 'jrs-signage-current-subtitle', ''); heading.appendChild(headingMain); appendText(heading, 'div', 'jrs-signage-updated', ''); right.appendChild(heading); var tabs = document.createElement('div'); tabs.className = 'jrs-signage-tabs'; items.forEach(function(item, index) { var tab = appendText(tabs, 'button', 'jrs-signage-tab', String(index + 1)); tab.type = 'button'; tab.setAttribute('aria-label', item.subTitle || item.title || ('項目 ' + (index + 1))); tab.onclick = function() { setSignageIndex(index, true); }; }); right.appendChild(tabs); var viewport = document.createElement('div'); viewport.className = 'jrs-signage-info-viewport'; right.appendChild(viewport); createSignageMapOverlay(root); document.body.appendChild(root); document.documentElement.classList.add('jrs-signage-active'); attachSignageScrollPause(root); renderSignageActiveItem(root); } applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); installSignageMode(); if (!window.__jrInfoBlinkTimer) { window.__jrInfoBlinkTimer = window.setInterval(function() { qa('.ml_seto, .ml_yosan1, .ml_yosan2, .ml_dosan1, .ml_dosan2, .ml_kotoku, .ml_mugi, .ml_tokushima, .ml_naruto, .ml_yodo').forEach(function(element) { element.style.visibility = element.style.visibility === 'hidden' ? 'visible' : 'hidden'; }); }, 1000); } if (!window.__jrInfoInstallTimer) { window.__jrInfoInstallTimer = window.setInterval(function() { applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); installSignageMode(); }, 1000); } if (!window.__jrInfoObserver && window.MutationObserver) { window.__jrInfoObserver = new MutationObserver(function() { applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); installSignageMode(); }); window.__jrInfoObserver.observe(document.body, { childList: true, subtree: true }); } if (!window.__jrSignageResizeHandler) { window.__jrSignageResizeHandler = function() { window.clearTimeout(window.__jrSignageResizeTimer); window.__jrSignageResizeTimer = window.setTimeout(function() { applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); installSignageMode(); }, 160); }; window.addEventListener('resize', window.__jrSignageResizeHandler); window.addEventListener('orientationchange', window.__jrSignageResizeHandler); } })(); true; `; }; export default function tndView() { const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount({ pingEnabled: true, backgroundThresholdMs: 60_000 }); const { navigate, addListener } = useNavigation(); const { fixed } = useThemeColors(); const { areaIconBadgeText } = useAreaInfo(); const insets = useSafeAreaInsets(); const windowSize = useWindowDimensions(); const [rootLayout, setRootLayout] = React.useState({ width: 0, height: 0 }); const layoutWidth = rootLayout.width || windowSize.width; const layoutHeight = rootLayout.height || windowSize.height; const isLandscape = layoutWidth > layoutHeight; const orientationKey = isLandscape ? "landscape" : "portrait"; // iOS can keep the previous orientation's safe-area axis during rotation. const notchInset = Platform.OS === "ios" ? Math.max(0, insets.top, insets.left, insets.right) : 0; const contentTop = Platform.OS === "ios" ? (isLandscape ? 0 : notchInset) : Math.max(0, insets.top); const contentLeft = 0; const contentRight = 0; const signageSafeLeft = Platform.OS === "ios" && isLandscape ? Math.max(0, insets.left, notchInset) : 0; const signageSafeRight = Platform.OS === "ios" && isLandscape ? Math.max(0, insets.right, notchInset) : 0; const reloadMargin = 10; const reloadTop = contentTop + (isLandscape ? reloadMargin : 0); const reloadRight = (isLandscape ? signageSafeRight : 0) + reloadMargin; const webViewContainerStyle: StyleProp = { flex: 1, paddingTop: contentTop, paddingLeft: contentLeft, paddingRight: contentRight, }; const layoutKey = [orientationKey, Math.round(layoutWidth), Math.round(layoutHeight), Math.round(contentTop), Math.round(signageSafeLeft), Math.round(signageSafeRight)].join("-"); const webViewKey = [remountKey, layoutKey].join("-"); const reloadButtonKey = [layoutKey, Math.round(reloadTop), Math.round(reloadRight)].join("-"); const bgAtRef = useRef(null); const [operationLandscapeEnabled, setOperationLandscapeEnabled] = React.useState(false); const [operationCaptureEnabled, setOperationCaptureEnabled] = React.useState(false); const operationLineBadges = useMemo(() => { const fallbackLineColors: Record = { G: "#4AA36B", }; const colors = lineColorList as Record; const seen = new Set(); return String(areaIconBadgeText || "") .split(",") .map((entry) => entry.trim().toUpperCase()) .filter((entry) => /^[A-Z]$/.test(entry)) .filter((entry) => { const hasColor = !!(colors[entry] || fallbackLineColors[entry]); if (!hasColor || seen.has(entry)) return false; seen.add(entry); return true; }) .map((code) => ({ code, color: colors[code] || fallbackLineColors[code], })); }, [areaIconBadgeText]); const operationPageScript = useMemo( () => buildOperationPageScript({ isLandscape, safeAreaLeft: signageSafeLeft, safeAreaRight: signageSafeRight, lineBadges: operationLineBadges, enableLandscapeMode: operationLandscapeEnabled, enableCapture: operationCaptureEnabled, }), [isLandscape, signageSafeLeft, signageSafeRight, operationLineBadges, operationLandscapeEnabled, operationCaptureEnabled] ); const reloadOpacity = useRef(new Animated.Value(1)).current; const reloadFadeTimerRef = useRef | null>(null); const reloadAnimationTokenRef = useRef(0); const [reloadButtonVisible, setReloadButtonVisible] = React.useState(true); const [keepAwakeEnabled, setKeepAwakeEnabled] = React.useState(true); const keepAwakeButtonRight = reloadRight + 60; const shouldKeepAwake = isLandscape && keepAwakeEnabled; const clearReloadFadeTimer = useCallback(() => { if (reloadFadeTimerRef.current) { clearTimeout(reloadFadeTimerRef.current); reloadFadeTimerRef.current = null; } }, []); const setReloadButtonFade = useCallback( (visible: boolean, duration: number) => { reloadAnimationTokenRef.current += 1; const token = reloadAnimationTokenRef.current; if (visible) { setReloadButtonVisible(true); } Animated.timing(reloadOpacity, { toValue: visible ? 1 : 0, duration, useNativeDriver: true, }).start(({ finished }) => { if (!finished) return; if (!visible && token === reloadAnimationTokenRef.current && isLandscape) { setReloadButtonVisible(false); } }); }, [isLandscape, reloadOpacity] ); const scheduleReloadButtonFade = useCallback( (delayMs = 2200) => { if (!isLandscape) return; clearReloadFadeTimer(); reloadFadeTimerRef.current = setTimeout(() => { setReloadButtonFade(false, 220); }, delayMs); }, [clearReloadFadeTimer, isLandscape, setReloadButtonFade] ); const registerScreenInteraction = useCallback(() => { if (!isLandscape) return; setReloadButtonFade(true, 180); scheduleReloadButtonFade(); }, [isLandscape, scheduleReloadButtonFade, setReloadButtonFade]); useEffect(() => { const sub = AppState.addEventListener("change", (state) => { if (state.match(/inactive|background/)) { bgAtRef.current = Date.now(); } }); return () => sub.remove(); }, []); const loadOperationInfoSettings = useCallback(async () => { try { const [landscapeValue, captureValue] = await Promise.all([ AS.getItem(STORAGE_KEYS.OPERATION_INFO_LANDSCAPE_ENABLED), AS.getItem(STORAGE_KEYS.OPERATION_INFO_CAPTURE_ENABLED), ]); setOperationLandscapeEnabled(landscapeValue === true || landscapeValue === "true"); setOperationCaptureEnabled(captureValue === true || captureValue === "true"); } catch { setOperationLandscapeEnabled(false); setOperationCaptureEnabled(false); } }, []); useEffect(() => { void loadOperationInfoSettings(); }, [loadOperationInfoSettings]); useFocusEffect( useCallback(() => { void loadOperationInfoSettings(); }, [loadOperationInfoSettings]) ); useFocusEffect( useCallback(() => { if (bgAtRef.current !== null) { const elapsed = Date.now() - bgAtRef.current; bgAtRef.current = null; if (elapsed > 3_000) remount(); } }, [remount]) ); useEffect(() => { const timer = setTimeout(() => { webViewRef.current?.injectJavaScript(operationPageScript); }, 250); return () => clearTimeout(timer); }, [orientationKey, layoutWidth, layoutHeight, contentTop, signageSafeLeft, signageSafeRight, operationPageScript, webViewRef]); useEffect(() => { let mounted = true; let timerId: ReturnType | null = null; const deactivate = () => { deactivateKeepAwake(KEEP_AWAKE_TAG).catch(() => {}); }; const activate = async () => { if (!mounted || AppState.currentState !== "active" || !shouldKeepAwake) { return; } try { await activateKeepAwakeAsync(KEEP_AWAKE_TAG); } catch (error) { if (!isActivityUnavailableError(error)) { console.warn("Failed to activate keep awake", error); } } }; if (shouldKeepAwake) { timerId = setTimeout(() => { void activate(); }, 250); } else { deactivate(); } const subscription = AppState.addEventListener("change", (state) => { if (state === "active" && shouldKeepAwake) { if (timerId) clearTimeout(timerId); timerId = setTimeout(() => { void activate(); }, 250); return; } deactivate(); }); return () => { mounted = false; if (timerId) { clearTimeout(timerId); } subscription.remove(); deactivate(); }; }, [shouldKeepAwake]); useEffect(() => { clearReloadFadeTimer(); setReloadButtonVisible(true); reloadOpacity.stopAnimation(); reloadOpacity.setValue(1); if (isLandscape) { scheduleReloadButtonFade(); } return clearReloadFadeTimer; }, [clearReloadFadeTimer, isLandscape, reloadOpacity, scheduleReloadButtonFade]); const saveOperationCapture = useCallback(async (dataUrl?: string, fileName?: string) => { if (!dataUrl?.startsWith("data:image/png;base64,")) { Alert.alert("切り出し失敗", "画像データの生成に失敗しました。"); return; } try { const sharingAvailable = await Sharing.isAvailableAsync(); if (!sharingAvailable) { Alert.alert("共有不可", "この端末では画像共有を利用できません。"); return; } const base64 = dataUrl.replace("data:image/png;base64,", ""); const safeFileName = (fileName ?? `operation-info-${Date.now()}.png`).replace(/[^\w.-]+/g, "_"); const fileUri = `${FileSystem.cacheDirectory}${safeFileName}`; await FileSystem.writeAsStringAsync(fileUri, base64, { encoding: FileSystem.EncodingType.Base64, }); await Sharing.shareAsync(fileUri, { mimeType: "image/png", dialogTitle: "運行情報を共有", }); } catch { Alert.alert("切り出し失敗", "画像の保存または共有に失敗しました。"); } }, []); const goToTrainMenu = () => { if (Platform.OS === "web") { Linking.openURL("https://www.jr-shikoku.co.jp/info/"); setTimeout(() => { // @ts-ignore navigate("topMenu", { screen: "menu" }); }, 100); } }; useEffect(() => { // @ts-ignore const unsubscribe = addListener("tabPress", goToTrainMenu); return unsubscribe; }, [addListener]); return ( { registerScreenInteraction(); return false; }} onTouchStart={registerScreenInteraction} onLayout={(event) => { const { width, height } = event.nativeEvent.layout; setRootLayout((current) => { if (Math.round(current.width) === Math.round(width) && Math.round(current.height) === Math.round(height)) { return current; } return { width, height }; }); }} style={{ backgroundColor: fixed.primary, flex: 1, overflow: "hidden", }} > remount()} {...processHandlers} {...pingHandlers} onLoadEnd={() => { pingHandlers.onLoadEnd?.(); webViewRef.current?.injectJavaScript(operationPageScript); registerScreenInteraction(); }} onMessage={async (event) => { pingHandlers.onMessage?.(event); try { const payload = JSON.parse(event.nativeEvent.data); if (payload.type !== "operationInfoCapture") { return; } if (payload.error) { Alert.alert("切り出し失敗", "項目画像の生成に失敗しました。"); return; } await saveOperationCapture(payload.dataUrl, payload.fileName); } catch { // __ping 以外の文字列メッセージは無視 } }} /> {isLandscape ? ( { registerScreenInteraction(); setKeepAwakeEnabled((current) => !current); }} top={reloadTop} right={keepAwakeButtonRight} opacity={reloadOpacity} visible={reloadButtonVisible} enabled={keepAwakeEnabled} /> ) : null} { registerScreenInteraction(); webViewRef.current?.reload(); }} top={reloadTop} right={reloadRight} opacity={reloadOpacity} visible={reloadButtonVisible} /> ); } type FloatingIconButtonProps = { onPress: () => void; top: number; right?: number; opacity: number | Animated.Value; visible?: boolean; backgroundColor: string; borderColor: string; iconName: React.ComponentProps["name"]; iconColor: string; }; type KeepAwakeButtonProps = { onPress: () => void; top: number; right?: number; opacity: number | Animated.Value; visible?: boolean; enabled?: boolean; }; type ReloadButtonProps = { onPress: () => void; top: number; right?: number; LoadError?: boolean; opacity: number | Animated.Value; visible?: boolean; }; const FloatingIconButton = ({ onPress, top, right = 0, opacity, visible = true, backgroundColor, borderColor, iconName, iconColor, }: FloatingIconButtonProps) => { const containerStyle = { position: "absolute" as const, top, right, opacity, zIndex: 1000, }; const styles: StyleProp = { width: 50, height: 50, backgroundColor, borderColor, borderStyle: "solid", borderWidth: 1, borderRadius: 50, alignContent: "center", alignSelf: "center", alignItems: "center", }; return ( ); }; const KeepAwakeButton = ({ onPress, top, right = 0, opacity, visible = true, enabled = true }: KeepAwakeButtonProps) => { const { fixed } = useThemeColors(); return ( ); }; const ReloadButton = ({ onPress, top, right = 0, LoadError = false, opacity, visible = true }: ReloadButtonProps) => { const { fixed } = useThemeColors(); return ( ); };