Update signage view configuration

This commit is contained in:
harukin-expo-dev-env
2026-06-29 14:42:44 +00:00
parent b783157553
commit 49aa33cbf5
2 changed files with 181 additions and 31 deletions
+6
View File
@@ -47,6 +47,12 @@
},
"production7.0": {
"channel": "familymart"
},
"beta7.1": {
"channel": "gmarket"
},
"production7.1": {
"channel": "geekbuying"
}
},
"submit": {
+175 -31
View File
@@ -13,6 +13,7 @@ import {
} from "react-native";
import * as FileSystem from "expo-file-system/legacy";
import * as Sharing from "expo-sharing";
import Share from "react-native-share";
import {
activateKeepAwakeAsync,
deactivateKeepAwake,
@@ -30,6 +31,17 @@ import lineColorList from "./assets/originData/lineColorList";
const KEEP_AWAKE_TAG = "operation-info-landscape";
type OperationCaptureImagePayload = {
dataUrl?: string;
fileName?: string;
};
type OperationCaptureBatchState = {
total: number;
files: Array<string | undefined>;
shared: boolean;
};
const isActivityUnavailableError = (error: unknown) =>
String(error).includes("The current activity is no longer available");
@@ -212,10 +224,12 @@ const buildOperationPageScript = (layout: {
}
var compat = getSignageCompatibilityProfile();
var rules = [
'.jrs-capture-page-wrap{display:block !important;text-align:right !important;margin:12px 0 10px !important;}',
'.jrs-capture-page-wrap{display:flex !important;flex-wrap:wrap !important;justify-content:flex-end !important;gap:8px !important;margin:12px 0 10px !important;}',
'.jrs-capture-page-link{display:inline-block !important;background:#0076a8 !important;color:#fff !important;padding:11px 16px !important;border-radius:999px !important;font-size:12px !important;font-weight:800 !important;line-height:1.2 !important;text-decoration:none !important;box-shadow:0 4px 12px rgba(0,118,168,.22) !important;position:relative !important;z-index:9999 !important;}',
'.jrs-capture-page-link:visited{color:#fff !important;}',
'.jrs-capture-page-link:active{opacity:.9 !important;transform:translateY(1px) !important;}',
'.jrs-capture-page-link.is-secondary{background:#ffffff !important;color:#0076a8 !important;border:1px solid #0099CB !important;box-shadow:none !important;}',
'.jrs-capture-page-link.is-secondary:visited{color:#0076a8 !important;}',
'.jrs-capture-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;}',
@@ -864,6 +878,40 @@ const buildOperationPageScript = (layout: {
}
}
async function renderAllItemsToSeparateImages(items, fileNameBase) {
try {
if (!items || !items.length) {
postMessage({ error: true });
return;
}
var batchId = 'operation-info-batch-' + Date.now() + '-' + Math.floor(Math.random() * 100000);
for (var index = 0; index < items.length; index += 1) {
var item = items[index];
var capture = await buildItemCaptureImageData(
item.title,
item.subTitle,
item.updatedAt,
item.blocks || [],
fileNameBase + '-' + (item.infoId || (index + 1))
);
if (!capture) {
postMessage({ error: true });
return;
}
postMessage({
batchId: batchId,
batchIndex: index,
batchTotal: items.length,
dataUrl: capture.dataUrl,
fileName: capture.fileName
});
}
} catch (e) {
postMessage({ error: true });
}
}
async function renderAllItemsToImage(items, statusText, fileNameBase) {
try {
if (!items || !items.length) {
@@ -1065,29 +1113,50 @@ const buildOperationPageScript = (layout: {
if (!infoRoot) return;
var items = getOperationItems();
var existingWrap = q('.jrs-capture-page-wrap');
var wrap = q('.jrs-capture-page-wrap');
if (!items.length) {
if (existingWrap && existingWrap.parentNode) existingWrap.parentNode.removeChild(existingWrap);
if (wrap && wrap.parentNode) wrap.parentNode.removeChild(wrap);
return;
}
if (existingWrap) return;
if (!wrap) {
wrap = document.createElement('div');
wrap.className = 'jrs-capture-page-wrap';
if (infoRoot.parentNode) {
infoRoot.parentNode.insertBefore(wrap, infoRoot);
}
}
var wrap = document.createElement('div');
wrap.className = 'jrs-capture-page-wrap';
var link = document.createElement('a');
link.href = '#';
link.className = 'jrs-capture-page-link';
link.textContent = '表示中の運行情報をすべて切り出す';
link.onclick = function(event) {
event.preventDefault();
event.stopPropagation();
renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now());
};
wrap.appendChild(link);
var allLink = q('.jrs-capture-page-link.is-all', wrap);
if (!allLink) {
allLink = document.createElement('a');
allLink.href = '#';
allLink.className = 'jrs-capture-page-link is-all';
allLink.textContent = '表示中の運行情報をすべて切り出す';
allLink.onclick = function(event) {
event.preventDefault();
event.stopPropagation();
renderAllItemsToImage(getOperationItems(), getStatusText(q('.delay_status')), 'operation-info-all-' + Date.now());
};
wrap.appendChild(allLink);
}
if (infoRoot.parentNode) {
infoRoot.parentNode.insertBefore(wrap, infoRoot);
var batchLink = q('.jrs-capture-page-link.is-batch', wrap);
if (items.length > 1) {
if (!batchLink) {
batchLink = document.createElement('a');
batchLink.href = '#';
batchLink.className = 'jrs-capture-page-link is-secondary is-batch';
batchLink.textContent = '項目ごとに複数画像で共有';
batchLink.onclick = function(event) {
event.preventDefault();
event.stopPropagation();
renderAllItemsToSeparateImages(getOperationItems(), 'operation-info-share-' + Date.now());
};
wrap.appendChild(batchLink);
}
} else if (batchLink && batchLink.parentNode) {
batchLink.parentNode.removeChild(batchLink);
}
}
@@ -1790,6 +1859,7 @@ export default function tndView() {
const webViewKey = [remountKey, layoutKey].join("-");
const reloadButtonKey = [layoutKey, Math.round(reloadTop), Math.round(reloadRight)].join("-");
const bgAtRef = useRef<number | null>(null);
const operationCaptureBatchesRef = useRef<Record<string, OperationCaptureBatchState>>({});
const [operationLandscapeEnabled, setOperationLandscapeEnabled] = React.useState(false);
const [operationCaptureEnabled, setOperationCaptureEnabled] = React.useState(false);
const operationLineBadges = useMemo(() => {
@@ -1991,35 +2061,105 @@ export default function tndView() {
return clearReloadFadeTimer;
}, [clearReloadFadeTimer, isLandscape, reloadOpacity, scheduleReloadButtonFade]);
const saveOperationCapture = useCallback(async (dataUrl?: string, fileName?: string) => {
const writeOperationCaptureFile = useCallback(async (dataUrl?: string, fileName?: string) => {
if (!dataUrl?.startsWith("data:image/png;base64,")) {
Alert.alert("切り出し失敗", "画像データの生成に失敗しました。");
throw new Error("invalid-image-data");
}
if (!FileSystem.cacheDirectory) {
throw new Error("cache-directory-unavailable");
}
const base64 = dataUrl.replace("data:image/png;base64,", "");
const uniquePrefix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const safeFileName = (fileName ?? `operation-info-${uniquePrefix}.png`).replace(/[^\w.-]+/g, "_");
const fileUri = `${FileSystem.cacheDirectory}${uniquePrefix}-${safeFileName}`;
await FileSystem.writeAsStringAsync(fileUri, base64, {
encoding: FileSystem.EncodingType.Base64,
});
return fileUri;
}, []);
const shareOperationCaptureFiles = useCallback(async (fileUris: string[]) => {
if (!fileUris.length) {
Alert.alert("切り出し失敗", "共有する画像が見つかりませんでした。");
return;
}
try {
if (fileUris.length === 1) {
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, {
await Sharing.shareAsync(fileUris[0], {
mimeType: "image/png",
dialogTitle: "運行情報を共有",
});
} catch {
Alert.alert("切り出し失敗", "画像の保存または共有に失敗しました。");
return;
}
await Share.open({
urls: fileUris,
type: "image/png",
title: "運行情報を共有",
failOnCancel: false,
});
}, []);
const saveOperationCapture = useCallback(
async (dataUrl?: string, fileName?: string) => {
try {
const fileUri = await writeOperationCaptureFile(dataUrl, fileName);
await shareOperationCaptureFiles([fileUri]);
} catch {
Alert.alert("切り出し失敗", "画像の保存または共有に失敗しました。");
}
},
[shareOperationCaptureFiles, writeOperationCaptureFile]
);
const saveOperationCaptureBatchItem = useCallback(
async (payload: OperationCaptureImagePayload & { batchId?: string; batchIndex?: number; batchTotal?: number }) => {
const batchId = typeof payload.batchId === "string" ? payload.batchId : "";
const batchTotal = Number(payload.batchTotal ?? 0);
const batchIndex = Number(payload.batchIndex ?? 0);
if (!batchId || batchTotal < 1 || batchIndex < 0 || batchIndex >= batchTotal) {
await saveOperationCapture(payload.dataUrl, payload.fileName);
return;
}
try {
const fileUri = await writeOperationCaptureFile(payload.dataUrl, payload.fileName);
const batches = operationCaptureBatchesRef.current;
const entry = batches[batchId] ?? {
total: batchTotal,
files: new Array(batchTotal),
shared: false,
};
entry.total = batchTotal;
if (!entry.files[batchIndex]) {
entry.files[batchIndex] = fileUri;
}
batches[batchId] = entry;
const readyFiles = entry.files.filter((uri): uri is string => !!uri);
if (!entry.shared && readyFiles.length === entry.total) {
entry.shared = true;
delete batches[batchId];
await shareOperationCaptureFiles(readyFiles);
}
} catch {
delete operationCaptureBatchesRef.current[batchId];
Alert.alert("切り出し失敗", "画像の保存または共有に失敗しました。");
}
},
[saveOperationCapture, shareOperationCaptureFiles, writeOperationCaptureFile]
);
const goToTrainMenu = () => {
if (Platform.OS === "web") {
Linking.openURL("https://www.jr-shikoku.co.jp/info/");
@@ -2091,6 +2231,10 @@ export default function tndView() {
Alert.alert("切り出し失敗", "項目画像の生成に失敗しました。");
return;
}
if (payload.batchId) {
await saveOperationCaptureBatchItem(payload);
return;
}
await saveOperationCapture(payload.dataUrl, payload.fileName);
} catch {
// __ping 以外の文字列メッセージは無視