From 9fe6f3ebf30db4ded18d3689d297aeec5892bbcd Mon Sep 17 00:00:00 2001 From: harukin-expo-dev-env Date: Fri, 10 Jul 2026 11:28:15 +0000 Subject: [PATCH] =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E7=AE=A1=E7=90=86=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=9D=E3=83=BC=E3=83=88=E3=83=A1=E3=82=BF?= =?UTF-8?q?=E3=82=BF=E3=82=B0=E3=81=AE=E7=AE=A1=E7=90=86=E3=82=92=E5=BC=B7?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GeneralWebView.tsx | 52 +++++++++++++++++++++++++--- lib/webViewInjectjavascript.ts | 4 +-- ndView.tsx | 63 ++++++++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/GeneralWebView.tsx b/GeneralWebView.tsx index a06dd5e..2d2f4b8 100644 --- a/GeneralWebView.tsx +++ b/GeneralWebView.tsx @@ -125,6 +125,8 @@ export default ({ route }) => { const { importRecordingsFromText } = useTrainMenu(); const webViewRef = React.useRef(null); const [canGoBack, setCanGoBack] = React.useState(false); + const nativeCanGoBackRef = React.useRef(false); + const historyStackRef = React.useRef([]); const [selectedEnvironment, setSelectedEnvironment] = React.useState( DEFAULT_JR_DATA_SYSTEM_ENV, ); @@ -162,6 +164,9 @@ export default ({ route }) => { if (!isMounted) return; const nextEnvironment = normalizeJrDataSystemEnvironment(value); const rawUri = typeof uri === "string" ? uri : ""; + historyStackRef.current = []; + nativeCanGoBackRef.current = false; + setCanGoBack(false); setSelectedEnvironment(nextEnvironment); setResolvedUri( importRecordingDownloads @@ -189,6 +194,41 @@ export default ({ route }) => { setWebViewKey((k) => k + 1); }; + const syncCanGoBack = React.useCallback((nativeCanGoBack: boolean) => { + nativeCanGoBackRef.current = nativeCanGoBack; + setCanGoBack(nativeCanGoBack || historyStackRef.current.length > 1); + }, []); + + const pushHistoryEntry = React.useCallback((url: string) => { + if (!url) return; + const stack = historyStackRef.current; + if (stack[stack.length - 1] === url) return; + stack.push(url); + if (stack.length > 30) { + stack.splice(0, stack.length - 30); + } + }, []); + + const handlePseudoBack = React.useCallback(() => { + const stack = historyStackRef.current; + if (stack.length <= 1) return false; + stack.pop(); + const previousUrl = stack[stack.length - 1]; + if (!previousUrl) return false; + setResolvedUri(previousUrl); + syncCanGoBack(false); + remount(); + return true; + }, [remount, syncCanGoBack]); + + const handleWebViewBack = React.useCallback(() => { + if (nativeCanGoBackRef.current) { + webViewRef.current?.goBack(); + return true; + } + return handlePseudoBack(); + }, [handlePseudoBack]); + const handleImportRecordingText = React.useCallback( async (content: string) => { try { @@ -231,8 +271,7 @@ export default ({ route }) => { React.useCallback(() => { const onHardwareBack = () => { if (canGoBack) { - webViewRef.current?.goBack(); - return true; + if (handleWebViewBack()) return true; } goBack(); return true; @@ -240,7 +279,7 @@ export default ({ route }) => { const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack); return () => subscription.remove(); - }, [canGoBack, goBack]) + }, [canGoBack, goBack, handleWebViewBack]) ); return ( @@ -325,13 +364,16 @@ export default ({ route }) => { } }} onNavigationStateChange={(navState) => { - setCanGoBack(navState.canGoBack); if (navState.url) { const nextUrl = importRecordingDownloads ? navState.url : rewriteJrDataSystemUrl(navState.url, selectedEnvironment); setResolvedUri((current) => (current === nextUrl ? current : nextUrl)); + if (nextUrl !== "https://unyohub.2pd.jp/integration/succeeded.php") { + pushHistoryEntry(nextUrl); + } } + syncCanGoBack(navState.canGoBack); // SPA内遷移中は白画面誤検知を防ぐためblankCountをリセット if (navState.loading) blankCountRef.current = 0; if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") { @@ -398,7 +440,7 @@ export default ({ route }) => { })(); return; } - if (type === "back") return webViewRef.current?.goBack(); + if (type === "back") return handleWebViewBack(); if (type === "windowClose") return goBack(); }} /> diff --git a/lib/webViewInjectjavascript.ts b/lib/webViewInjectjavascript.ts index 5b3215e..ae567c0 100644 --- a/lib/webViewInjectjavascript.ts +++ b/lib/webViewInjectjavascript.ts @@ -945,7 +945,7 @@ export const injectJavascriptData = ({ const optionalTextColor = optionalText.includes("最終") ? "red" : "black"; const trainTypeTextStyle = "font-size:10px;font-weight:bold;font-style:italic;padding:0;margin:0;color:#ffffff;text-align:center;line-height:1;"; - const trainTypeFrameStyle = "position:relative;height:12px;width:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;"; + const trainTypeFrameStyle = "position:relative;height:10px;width:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;"; const trainTypeMainStyle = trainTypeTextStyle + "display:flex;align-items:center;justify-content:center;width:100%;height:100%;"; const trainTypeWanmanStyle = trainTypeMainStyle.replace("font-size:10px;", "font-size:9px;"); const trainTypeHtml = isOneManType @@ -997,7 +997,7 @@ export const injectJavascriptData = ({ badgeHtml += "
"; } - 行き先情報.insertAdjacentHTML('beforebegin', "
" + badgeHtml + "

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + viaData + "

" + optionalText + "

" + trainName + "

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

" + trainTypeHtml + "

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

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

" + (TrainNumberOverride ? TrainNumberOverride : TrainNumber) + "

" + viaData + "

" + optionalText + "

" + trainName + "

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

" + trainTypeHtml + "

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

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

" + returnText1 + "

"); diff --git a/ndView.tsx b/ndView.tsx index 33900bd..e1432c8 100644 --- a/ndView.tsx +++ b/ndView.tsx @@ -167,6 +167,21 @@ const buildOperationPageScript = (layout: { }); } + function ensureViewportMeta() { + var head = document.head || q('head'); + if (!head) return; + var meta = q('meta[name="viewport"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + head.appendChild(meta); + } + var content = 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover'; + if (meta.getAttribute('content') !== content) { + meta.setAttribute('content', content); + } + } + function applyLayoutTweaks() { setStyles('.sitettl', { display: 'none' }); setStyles('.attention', { display: 'none', width: '100vw' }); @@ -1172,7 +1187,7 @@ const buildOperationPageScript = (layout: { function installCaptureButtons() { var nativeLayout = window.__jrsNativeOperationLayout || {}; - if (!nativeLayout.enableCapture) { + if (!nativeLayout.enableCapture || isSignageViewport()) { removeCaptureButtons(); return; } @@ -1624,8 +1639,8 @@ const buildOperationPageScript = (layout: { return; } if (!isSignageViewport()) { - state.viewportMisses += 1; - if (nativeLayout.forceSignage !== true && state.viewportMisses >= 3) removeSignageMode(); + state.viewportMisses = 0; + removeSignageMode(); return; } state.viewportMisses = 0; @@ -1782,6 +1797,7 @@ const buildOperationPageScript = (layout: { renderSignageActiveItem(root); } + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1797,6 +1813,7 @@ const buildOperationPageScript = (layout: { if (!window.__jrInfoInstallTimer) { window.__jrInfoInstallTimer = window.setInterval(function() { + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1806,6 +1823,7 @@ const buildOperationPageScript = (layout: { if (!window.__jrInfoObserver && window.MutationObserver) { window.__jrInfoObserver = new MutationObserver(function() { + ensureViewportMeta(); applyLayoutTweaks(); ensureStyles(); installCaptureButtons(); @@ -1940,6 +1958,7 @@ export default function tndView() { const operationScriptRetryTimerRef = useRef | null>(null); const operationScriptRetryCountRef = useRef(0); const pendingFocusedRemountRef = useRef(false); + const lastOrientationKeyRef = useRef(null); const clearOperationScriptRetry = useCallback(() => { if (operationScriptRetryTimerRef.current) { @@ -2257,6 +2276,44 @@ export default function tndView() { return clearOperationScriptRetry; }, [clearOperationScriptRetry, contentTop, hasActivatedWebView, isFocused, layoutHeight, layoutWidth, orientationKey, scheduleOperationScriptInjection, shouldInjectOperationScript, signageSafeLeft, signageSafeRight]); + useEffect(() => { + if (!hasActivatedWebView) { + lastOrientationKeyRef.current = orientationKey; + return; + } + + if (lastOrientationKeyRef.current === null) { + lastOrientationKeyRef.current = orientationKey; + return; + } + + if (lastOrientationKeyRef.current === orientationKey) { + return; + } + + const previousOrientation = lastOrientationKeyRef.current; + lastOrientationKeyRef.current = orientationKey; + + Sentry.addBreadcrumb({ + category: "operation.webview", + level: "info", + message: "operation orientation remount", + data: { + previousOrientation, + orientation: orientationKey, + landscapeMode: operationLandscapeEnabled, + platform: Platform.OS, + isPad: Platform.OS === "ios" ? Platform.isPad : false, + }, + }); + + const timer = setTimeout(() => { + remount(); + }, 120); + + return () => clearTimeout(timer); + }, [hasActivatedWebView, operationLandscapeEnabled, orientationKey, remount]); + useEffect(() => { let mounted = true; let timerId: ReturnType | null = null;