2026-06-10 05:04:09 +00:00
|
|
|
import React, { useRef, useEffect, useCallback, useMemo } from "react";
|
2025-09-11 16:55:05 +00:00
|
|
|
import {
|
2026-06-10 05:04:09 +00:00
|
|
|
Alert,
|
2026-06-20 06:07:39 +00:00
|
|
|
Animated,
|
2025-09-11 16:55:05 +00:00
|
|
|
View,
|
|
|
|
|
Platform,
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
StyleProp,
|
|
|
|
|
ViewStyle,
|
|
|
|
|
Linking,
|
2026-05-01 12:35:36 +00:00
|
|
|
AppState,
|
2026-06-20 06:07:39 +00:00
|
|
|
useWindowDimensions,
|
2025-09-11 16:55:05 +00:00
|
|
|
} from "react-native";
|
2026-06-10 05:04:09 +00:00
|
|
|
import * as FileSystem from "expo-file-system/legacy";
|
|
|
|
|
import * as Sharing from "expo-sharing";
|
2026-06-20 06:07:39 +00:00
|
|
|
import {
|
|
|
|
|
activateKeepAwakeAsync,
|
|
|
|
|
deactivateKeepAwake,
|
|
|
|
|
} from "expo-keep-awake";
|
2022-09-28 03:19:04 +09:00
|
|
|
import { WebView } from "react-native-webview";
|
2023-12-23 03:12:02 +09:00
|
|
|
import { Ionicons } from "@expo/vector-icons";
|
2026-05-01 12:35:36 +00:00
|
|
|
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
2026-03-17 22:19:46 +00:00
|
|
|
import { useThemeColors } from "@/lib/theme";
|
2026-06-25 14:27:24 +00:00
|
|
|
import { AS } from "./storageControl";
|
|
|
|
|
import { STORAGE_KEYS } from "@/constants";
|
2026-03-20 07:14:58 +00:00
|
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
2026-05-01 10:43:19 +00:00
|
|
|
import { useWebViewRemount } from "@/lib/useWebViewRemount";
|
2026-06-20 06:07:39 +00:00
|
|
|
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");
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
const buildOperationPageScript = (layout: {
|
|
|
|
|
isLandscape: boolean;
|
|
|
|
|
safeAreaLeft: number;
|
|
|
|
|
safeAreaRight: number;
|
|
|
|
|
lineBadges: Array<{ code: string; color: string }>;
|
2026-06-25 14:27:24 +00:00
|
|
|
enableLandscapeMode: boolean;
|
|
|
|
|
enableCapture: boolean;
|
2026-06-20 06:07:39 +00:00
|
|
|
}) => {
|
|
|
|
|
const nativeLayout = JSON.stringify({
|
2026-06-25 14:27:24 +00:00
|
|
|
forceSignage: layout.enableLandscapeMode && layout.isLandscape,
|
|
|
|
|
enableLandscapeMode: layout.enableLandscapeMode,
|
|
|
|
|
enableCapture: layout.enableCapture,
|
2026-06-20 06:07:39 +00:00
|
|
|
safeAreaLeft: Math.max(0, Math.round(layout.safeAreaLeft)),
|
|
|
|
|
safeAreaRight: Math.max(0, Math.round(layout.safeAreaRight)),
|
|
|
|
|
lineBadges: layout.lineBadges,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return String.raw`
|
2026-06-10 05:04:09 +00:00
|
|
|
(function() {
|
2026-06-20 06:07:39 +00:00
|
|
|
window.__jrsNativeOperationLayout = ${nativeLayout};
|
2026-06-10 05:04:09 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
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() {
|
2026-06-20 06:07:39 +00:00
|
|
|
var style = q('#jrs-operation-capture-style');
|
|
|
|
|
if (!style) {
|
|
|
|
|
style = document.createElement('style');
|
|
|
|
|
style.id = 'jrs-operation-capture-style';
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
style.textContent = [
|
2026-06-20 06:07:39 +00:00
|
|
|
'.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;}',
|
2026-06-10 05:04:09 +00:00
|
|
|
'.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;}',
|
2026-06-20 06:07:39 +00:00
|
|
|
'.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}}'
|
2026-06-10 05:04:09 +00:00
|
|
|
].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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 16:57:20 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 16:57:20 +00:00
|
|
|
var blocks = parseDetailBlocks(detailHtml);
|
|
|
|
|
var sections = splitCaptureBlocksIntoSections(blocks);
|
|
|
|
|
var useSectionColumns = sections.length > 1;
|
|
|
|
|
|
|
|
|
|
var singleWidth = 1080;
|
2026-06-10 05:04:09 +00:00
|
|
|
var paddingX = 46;
|
|
|
|
|
var paddingY = 38;
|
2026-06-25 16:57:20 +00:00
|
|
|
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;
|
2026-06-10 05:04:09 +00:00
|
|
|
var mapCanvas = null;
|
|
|
|
|
try {
|
|
|
|
|
mapCanvas = await buildMapImage(contentWidth);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
mapCanvas = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var layout = [];
|
|
|
|
|
var y = paddingY;
|
|
|
|
|
|
|
|
|
|
if (mapCanvas) {
|
2026-06-20 06:07:39 +00:00
|
|
|
layout.push({ type: 'map', x: paddingX, y: y, width: contentWidth, height: mapCanvas.height, canvas: mapCanvas });
|
2026-06-10 05:04:09 +00:00
|
|
|
y += mapCanvas.height + 28;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 16:57:20 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
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);
|
2026-06-20 06:07:39 +00:00
|
|
|
drawCaptureLayout(ctx, layout, width, contentWidth, paddingX);
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
postMessage({
|
|
|
|
|
dataUrl: canvas.toDataURL('image/png'),
|
|
|
|
|
fileName: fileNameBase + '.png'
|
2026-06-10 05:04:09 +00:00
|
|
|
});
|
2026-06-20 06:07:39 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
postMessage({ error: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
async function renderAllItemsToImage(items, statusText, fileNameBase) {
|
|
|
|
|
try {
|
|
|
|
|
if (!items || !items.length) {
|
|
|
|
|
postMessage({ error: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
var canvas = document.createElement('canvas');
|
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
|
if (!ctx) {
|
|
|
|
|
postMessage({ error: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
var singleWidth = 1080;
|
|
|
|
|
var paddingX = 46;
|
|
|
|
|
var paddingY = 38;
|
|
|
|
|
var singleContentWidth = singleWidth - paddingX * 2;
|
|
|
|
|
var columnGap = 24;
|
2026-06-25 13:26:51 +00:00
|
|
|
var maxColumns = 3;
|
|
|
|
|
var columnCount = Math.max(1, Math.min(maxColumns, items.length));
|
2026-06-20 06:07:39 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
if (mapCanvas) {
|
|
|
|
|
layout.push({ type: 'map', x: paddingX, y: y, width: contentWidth, height: mapCanvas.height, canvas: mapCanvas });
|
|
|
|
|
y += mapCanvas.height + 28;
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
if (strip(statusText)) {
|
|
|
|
|
y = appendCaptureItemLayout(layout, ctx, {
|
|
|
|
|
title: statusText,
|
|
|
|
|
subTitle: '表示中の運行情報一覧',
|
|
|
|
|
updatedAt: '',
|
|
|
|
|
blocks: []
|
|
|
|
|
}, y, { x: paddingX, width: contentWidth, showDetailBox: false, sectionGap: 24 });
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
var columnHeights = [];
|
2026-06-25 13:26:51 +00:00
|
|
|
var columnIndex;
|
|
|
|
|
for (columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
|
2026-06-20 06:07:39 +00:00
|
|
|
columnHeights.push(y);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 13:26:51 +00:00
|
|
|
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, {
|
2026-06-20 06:07:39 +00:00
|
|
|
x: itemX,
|
|
|
|
|
width: itemWidth,
|
|
|
|
|
innerPaddingX: 30,
|
|
|
|
|
sectionGap: 26
|
|
|
|
|
});
|
2026-06-10 05:04:09 +00:00
|
|
|
});
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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);
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 14:27:24 +00:00
|
|
|
function removeCaptureButtons() {
|
|
|
|
|
removeNodes(document, '.jrs-capture-page-wrap, .jrs-capture-wrap, .jrs-subcapture-wrap');
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
function installCaptureButtons() {
|
2026-06-25 14:27:24 +00:00
|
|
|
var nativeLayout = window.__jrsNativeOperationLayout || {};
|
|
|
|
|
if (!nativeLayout.enableCapture) {
|
|
|
|
|
removeCaptureButtons();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
installPageCaptureButton();
|
2026-06-10 05:04:09 +00:00
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
function isSignageViewport() {
|
|
|
|
|
var nativeLayout = window.__jrsNativeOperationLayout || {};
|
2026-06-25 14:27:24 +00:00
|
|
|
if (!nativeLayout.enableLandscapeMode) return false;
|
2026-06-20 06:07:39 +00:00
|
|
|
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 || {};
|
2026-06-25 14:27:24 +00:00
|
|
|
if (!nativeLayout.enableLandscapeMode) {
|
|
|
|
|
state.viewportMisses = 0;
|
|
|
|
|
removeSignageMode();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-20 06:07:39 +00:00
|
|
|
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) {
|
2026-06-25 13:26:51 +00:00
|
|
|
var button = appendText(
|
|
|
|
|
sideList,
|
|
|
|
|
'button',
|
|
|
|
|
'jrs-signage-side-item',
|
|
|
|
|
item.subTitle || item.title || ('項目 ' + (index + 1))
|
|
|
|
|
);
|
2026-06-20 06:07:39 +00:00
|
|
|
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';
|
2026-06-25 13:26:51 +00:00
|
|
|
tab.setAttribute('aria-label', item.subTitle || item.title || ('項目 ' + (index + 1)));
|
2026-06-20 06:07:39 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
applyLayoutTweaks();
|
|
|
|
|
ensureStyles();
|
|
|
|
|
installCaptureButtons();
|
2026-06-20 06:07:39 +00:00
|
|
|
installSignageMode();
|
2026-06-10 05:04:09 +00:00
|
|
|
|
|
|
|
|
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();
|
2026-06-20 06:07:39 +00:00
|
|
|
installSignageMode();
|
2026-06-10 05:04:09 +00:00
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!window.__jrInfoObserver && window.MutationObserver) {
|
|
|
|
|
window.__jrInfoObserver = new MutationObserver(function() {
|
|
|
|
|
applyLayoutTweaks();
|
|
|
|
|
ensureStyles();
|
|
|
|
|
installCaptureButtons();
|
2026-06-20 06:07:39 +00:00
|
|
|
installSignageMode();
|
2026-06-10 05:04:09 +00:00
|
|
|
});
|
|
|
|
|
window.__jrInfoObserver.observe(document.body, { childList: true, subtree: true });
|
|
|
|
|
}
|
2026-06-20 06:07:39 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-06-10 05:04:09 +00:00
|
|
|
})();
|
|
|
|
|
true;
|
|
|
|
|
`;
|
2026-06-20 06:07:39 +00:00
|
|
|
};
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2022-09-28 03:19:04 +09:00
|
|
|
export default function tndView() {
|
2026-06-10 05:04:09 +00:00
|
|
|
const { remountKey, remount, processHandlers, pingHandlers, webViewRef } = useWebViewRemount({ pingEnabled: true, backgroundThresholdMs: 60_000 });
|
|
|
|
|
const { navigate, addListener } = useNavigation();
|
2026-03-17 22:19:46 +00:00
|
|
|
const { fixed } = useThemeColors();
|
2026-06-20 06:07:39 +00:00
|
|
|
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<ViewStyle> = {
|
|
|
|
|
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("-");
|
2026-05-01 12:35:36 +00:00
|
|
|
const bgAtRef = useRef<number | null>(null);
|
2026-06-25 14:27:24 +00:00
|
|
|
const [operationLandscapeEnabled, setOperationLandscapeEnabled] = React.useState(false);
|
|
|
|
|
const [operationCaptureEnabled, setOperationCaptureEnabled] = React.useState(false);
|
2026-06-20 06:07:39 +00:00
|
|
|
const operationLineBadges = useMemo(() => {
|
|
|
|
|
const fallbackLineColors: Record<string, string> = {
|
|
|
|
|
G: "#4AA36B",
|
|
|
|
|
};
|
|
|
|
|
const colors = lineColorList as Record<string, string>;
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
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(
|
2026-06-25 14:27:24 +00:00
|
|
|
() =>
|
|
|
|
|
buildOperationPageScript({
|
|
|
|
|
isLandscape,
|
|
|
|
|
safeAreaLeft: signageSafeLeft,
|
|
|
|
|
safeAreaRight: signageSafeRight,
|
|
|
|
|
lineBadges: operationLineBadges,
|
|
|
|
|
enableLandscapeMode: operationLandscapeEnabled,
|
|
|
|
|
enableCapture: operationCaptureEnabled,
|
|
|
|
|
}),
|
|
|
|
|
[isLandscape, signageSafeLeft, signageSafeRight, operationLineBadges, operationLandscapeEnabled, operationCaptureEnabled]
|
2026-06-20 06:07:39 +00:00
|
|
|
);
|
|
|
|
|
const reloadOpacity = useRef(new Animated.Value(1)).current;
|
|
|
|
|
const reloadFadeTimerRef = useRef<ReturnType<typeof setTimeout> | 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]);
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-05-01 12:35:36 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const sub = AppState.addEventListener("change", (state) => {
|
|
|
|
|
if (state.match(/inactive|background/)) {
|
|
|
|
|
bgAtRef.current = Date.now();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return () => sub.remove();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-06-25 14:27:24 +00:00
|
|
|
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])
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-01 12:35:36 +00:00
|
|
|
useFocusEffect(
|
|
|
|
|
useCallback(() => {
|
|
|
|
|
if (bgAtRef.current !== null) {
|
|
|
|
|
const elapsed = Date.now() - bgAtRef.current;
|
|
|
|
|
bgAtRef.current = null;
|
|
|
|
|
if (elapsed > 3_000) remount();
|
|
|
|
|
}
|
|
|
|
|
}, [remount])
|
|
|
|
|
);
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
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<typeof setTimeout> | 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]);
|
|
|
|
|
|
2026-06-10 05:04:09 +00:00
|
|
|
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("切り出し失敗", "画像の保存または共有に失敗しました。");
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-09-11 16:55:05 +00:00
|
|
|
const goToTrainMenu = () => {
|
|
|
|
|
if (Platform.OS === "web") {
|
|
|
|
|
Linking.openURL("https://www.jr-shikoku.co.jp/info/");
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
navigate("topMenu", { screen: "menu" });
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-10 05:04:09 +00:00
|
|
|
|
2025-09-11 16:55:05 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const unsubscribe = addListener("tabPress", goToTrainMenu);
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
}, [addListener]);
|
|
|
|
|
|
2021-04-02 18:38:05 +09:00
|
|
|
return (
|
2022-09-28 03:19:04 +09:00
|
|
|
<View
|
2026-06-20 06:07:39 +00:00
|
|
|
onStartShouldSetResponderCapture={() => {
|
|
|
|
|
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 };
|
|
|
|
|
});
|
|
|
|
|
}}
|
2022-09-28 03:19:04 +09:00
|
|
|
style={{
|
2026-03-17 22:19:46 +00:00
|
|
|
backgroundColor: fixed.primary,
|
2026-06-20 06:07:39 +00:00
|
|
|
flex: 1,
|
|
|
|
|
overflow: "hidden",
|
2022-09-28 03:19:04 +09:00
|
|
|
}}
|
|
|
|
|
>
|
2026-06-20 06:07:39 +00:00
|
|
|
<View style={webViewContainerStyle}>
|
|
|
|
|
<WebView
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
key={webViewKey}
|
|
|
|
|
ref={webViewRef}
|
|
|
|
|
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
|
|
|
|
|
originWhitelist={["https://www.jr-shikoku.co.jp"]}
|
|
|
|
|
mixedContentMode={"compatibility"}
|
|
|
|
|
contentMode="mobile"
|
|
|
|
|
automaticallyAdjustContentInsets={false}
|
|
|
|
|
contentInsetAdjustmentBehavior="never"
|
|
|
|
|
javaScriptEnabled
|
|
|
|
|
injectedJavaScript={operationPageScript}
|
|
|
|
|
pullToRefreshEnabled
|
|
|
|
|
onError={() => 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 以外の文字列メッセージは無視
|
2026-06-10 05:04:09 +00:00
|
|
|
}
|
2026-06-20 06:07:39 +00:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</View>
|
|
|
|
|
{isLandscape ? (
|
|
|
|
|
<KeepAwakeButton
|
|
|
|
|
onPress={() => {
|
|
|
|
|
registerScreenInteraction();
|
|
|
|
|
setKeepAwakeEnabled((current) => !current);
|
|
|
|
|
}}
|
|
|
|
|
top={reloadTop}
|
|
|
|
|
right={keepAwakeButtonRight}
|
|
|
|
|
opacity={reloadOpacity}
|
|
|
|
|
visible={reloadButtonVisible}
|
|
|
|
|
enabled={keepAwakeEnabled}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
<ReloadButton
|
|
|
|
|
key={reloadButtonKey}
|
|
|
|
|
onPress={() => {
|
|
|
|
|
registerScreenInteraction();
|
|
|
|
|
webViewRef.current?.reload();
|
2025-07-15 04:51:50 +00:00
|
|
|
}}
|
2026-06-20 06:07:39 +00:00
|
|
|
top={reloadTop}
|
|
|
|
|
right={reloadRight}
|
|
|
|
|
opacity={reloadOpacity}
|
|
|
|
|
visible={reloadButtonVisible}
|
2022-09-28 03:19:04 +09:00
|
|
|
/>
|
2021-04-02 18:38:05 +09:00
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-12-23 03:12:02 +09:00
|
|
|
|
2026-06-20 06:07:39 +00:00
|
|
|
type FloatingIconButtonProps = {
|
|
|
|
|
onPress: () => void;
|
|
|
|
|
top: number;
|
|
|
|
|
right?: number;
|
|
|
|
|
opacity: number | Animated.Value;
|
|
|
|
|
visible?: boolean;
|
|
|
|
|
backgroundColor: string;
|
|
|
|
|
borderColor: string;
|
|
|
|
|
iconName: React.ComponentProps<typeof Ionicons>["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,
|
2024-03-19 11:55:31 +00:00
|
|
|
top,
|
2026-06-20 06:07:39 +00:00
|
|
|
right,
|
|
|
|
|
opacity,
|
|
|
|
|
zIndex: 1000,
|
|
|
|
|
};
|
|
|
|
|
const styles: StyleProp<ViewStyle> = {
|
2024-03-19 11:55:31 +00:00
|
|
|
width: 50,
|
|
|
|
|
height: 50,
|
2026-06-20 06:07:39 +00:00
|
|
|
backgroundColor,
|
|
|
|
|
borderColor,
|
2024-03-19 11:55:31 +00:00
|
|
|
borderStyle: "solid",
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderRadius: 50,
|
|
|
|
|
alignContent: "center",
|
|
|
|
|
alignSelf: "center",
|
|
|
|
|
alignItems: "center",
|
2023-12-23 03:12:02 +09:00
|
|
|
};
|
|
|
|
|
return (
|
2026-06-20 06:07:39 +00:00
|
|
|
<Animated.View pointerEvents={visible ? "auto" : "none"} style={containerStyle}>
|
|
|
|
|
<TouchableOpacity onPress={onPress} style={styles}>
|
|
|
|
|
<View style={{ flex: 1 }} />
|
|
|
|
|
<Ionicons name={iconName} color={iconColor} size={30} />
|
|
|
|
|
<View style={{ flex: 1 }} />
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</Animated.View>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const KeepAwakeButton = ({ onPress, top, right = 0, opacity, visible = true, enabled = true }: KeepAwakeButtonProps) => {
|
|
|
|
|
const { fixed } = useThemeColors();
|
|
|
|
|
return (
|
|
|
|
|
<FloatingIconButton
|
|
|
|
|
onPress={onPress}
|
|
|
|
|
top={top}
|
|
|
|
|
right={right}
|
|
|
|
|
opacity={opacity}
|
|
|
|
|
visible={visible}
|
|
|
|
|
backgroundColor={enabled ? fixed.primary : "rgba(15, 23, 32, 0.72)"}
|
|
|
|
|
borderColor={fixed.textOnPrimary}
|
|
|
|
|
iconName={enabled ? "cafe" : "cafe-outline"}
|
|
|
|
|
iconColor={fixed.textOnPrimary}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ReloadButton = ({ onPress, top, right = 0, LoadError = false, opacity, visible = true }: ReloadButtonProps) => {
|
|
|
|
|
const { fixed } = useThemeColors();
|
|
|
|
|
return (
|
|
|
|
|
<FloatingIconButton
|
|
|
|
|
onPress={onPress}
|
|
|
|
|
top={top}
|
|
|
|
|
right={right}
|
|
|
|
|
opacity={opacity}
|
|
|
|
|
visible={visible}
|
|
|
|
|
backgroundColor={LoadError ? "red" : fixed.primary}
|
|
|
|
|
borderColor={fixed.textOnPrimary}
|
|
|
|
|
iconName="reload"
|
|
|
|
|
iconColor={fixed.textOnPrimary}
|
|
|
|
|
/>
|
2023-12-23 03:12:02 +09:00
|
|
|
);
|
|
|
|
|
};
|