Compare commits

..

255 Commits

Author SHA1 Message Date
harukin-expo-dev-env 907ddf9dae Merge commit '929c444474a121b15a8f995351387646fdb35e88' into develop 2026-07-15 08:04:16 +00:00
harukin-expo-dev-env 929c444474 fix: 通過駅のフィルタリングロジックを改善し、列車データの取得処理を最適化 2026-07-15 08:04:05 +00:00
harukin-expo-dev-env b6800d6566 fix: 列車情報の時間フィルタリングロジックを改善し、再生中の基準時刻を追加 2026-07-14 10:49:31 +00:00
harukin-expo-dev-env b0a0d62344 fix: 列車情報の表示ロジックを改善し、条件付きHTML生成を追加 2026-07-14 09:11:23 +00:00
harukin-expo-dev-env bfd90b50e4 feat: 列車タイプのラッピング処理を追加し、表示ロジックを改善 2026-07-14 05:57:05 +00:00
harukin-expo-dev-env 553735ac1d feat: JR-WEST-PLUSフォントの調整と表示領域の正規化を実施 2026-07-14 04:26:38 +00:00
harukin-expo-dev-env 5f1641ff90 feat: JR-WEST-PLUSフォントを追加し、フォント設定を最適化 2026-07-14 02:51:41 +00:00
harukin-expo-dev-env 351854f128 fix: 修正された列車データの取得と表示ロジックを改善し、未登録列車の処理を追加 2026-07-14 00:46:35 +00:00
harukin-expo-dev-env e6f0391484 feat: Implement X投稿向け運行情報画像セット generation in ndView.tsx
- Added functionality to generate a set of images for posting to X (formerly Twitter) with multiple images.
- Introduced a new button for creating X投稿向け画像, which generates up to 4 PNG images.
- The first image serves as a cover with a route map and summary, while subsequent images contain detailed operation information.
- Ensured images maintain a fixed size of 1080 x 1440 px and follow specific naming conventions.
- Implemented error handling for image generation failures and ensured compatibility with existing sharing features.
- Updated UI elements and styles for the new functionality, including button states and layout adjustments.
- Added necessary TypeScript type checks and validation for the new features.
2026-07-12 19:01:51 +00:00
harukin-expo-dev-env b1cfad4528 Merge commit '76b13a70a9e8f791bc8551107b7609581e92e8d5' into develop 2026-07-12 15:40:52 +00:00
harukin-expo-dev-env 76b13a70a9 型定義を改善し、originalStationListの使用を最適化 2026-07-12 15:35:00 +00:00
harukin-expo-dev-env eec2fd2bb7 Refactor MenuPage component and update dependencies
- Removed unused navigation logic from MenuPage.tsx.
- Added postinstall script for patch-package in package.json.
- Introduced a new patch for @react-navigation/bottom-tabs to fix zIndex and opacity issues on Android.
- Updated yarn.lock with new dependencies and versions, including patch-package and others.
2026-07-12 10:59:40 +00:00
harukin-expo-dev-env 8c2930c814 ネイティブスクリーンの無効化を条件化し、スクリーンの非アクティブ状態を保持する設定を追加 2026-07-11 04:27:16 +00:00
harukin-expo-dev-env c7fdf6f0b1 Merge commit '9746a4d04168112371c44a7a1afd72cd8dd04e4f' into develop 2026-07-11 00:14:03 +00:00
harukin-expo-dev-env 9746a4d041 iOSおよびAndroidのビルド番号とバージョンコードを更新 2026-07-10 11:33:25 +00:00
harukin-expo-dev-env 740c825122 Merge commit 'f6b25ce5d30f7b5f85cb95c7d2e62432eb6aa1ca' into patch/6.x 2026-07-10 11:30:13 +00:00
harukin-expo-dev-env f6b25ce5d3 Merge commit '390ab3ac6932cbb0a25d179a0e8d81c061d8f268' into develop 2026-07-10 11:28:33 +00:00
harukin-expo-dev-env 390ab3ac69 履歴管理機能を追加し、ビューポートメタタグの管理を強化 2026-07-10 11:28:15 +00:00
harukin-expo-dev-env 2eb926890c Adjust train type badge rendering 2026-07-09 10:15:49 +00:00
harukin-expo-dev-env bf2e68a3a5 各種機能の改善と最適化、ナビゲーションの明示的なターゲット設定を追加 2026-07-08 14:39:15 +00:00
harukin-expo-dev-env dd4892f6b7 各種データの取得フラグ機能を実装、動作の最適化 2026-07-08 01:39:29 +00:00
harukin-expo-dev-env 61548ea3f9 Tune data fetch observability after first run 2026-07-07 04:10:24 +00:00
harukin-expo-dev-env bf0c4d90d9 Add data fetch observability 2026-07-07 02:47:13 +00:00
harukin-expo-dev-env 47d6f043b4 Work around Android user location map crash 2026-07-06 17:44:15 +00:00
harukin-expo-dev-env aacae06743 Initialize root tab state on startup 2026-07-06 08:42:28 +00:00
harukin-expo-dev-env dab60c47fa Tighten hidden tab preload timing 2026-07-06 08:31:08 +00:00
harukin-expo-dev-env 39fe154b33 Add hidden webview startup preload 2026-07-06 08:25:25 +00:00
harukin-expo-dev-env a8582c9fd6 Run hidden prewarm without interaction gate 2026-07-06 07:50:39 +00:00
harukin-expo-dev-env 3133535606 Prewarm operation tab from top menu startup 2026-07-06 07:01:06 +00:00
harukin-expo-dev-env a5e1b7d297 Restore Android carousel parallax mode 2026-07-06 06:20:13 +00:00
harukin-expo-dev-env 021a9b0101 Restore Android carousel and add runtime flags 2026-07-06 05:58:03 +00:00
harukin-expo-dev-env 6cea1ed4e8 Disable iOS screen optimizations for webviews 2026-07-04 16:33:33 +00:00
harukin-expo-dev-env 17f4db58bc Stabilize root tab screen mounting 2026-07-04 16:26:59 +00:00
harukin-expo-dev-env 9740a7639b Stabilize positions webview retention on iOS 2026-07-04 16:22:35 +00:00
harukin-expo-dev-env 191a536f5c Guard iOS webview remount on tab blur 2026-07-04 15:57:04 +00:00
harukin-expo-dev-env 07514bf92d Preserve operation webview and quiet noisy defaults 2026-07-04 15:48:37 +00:00
harukin-expo-dev-env 997d910d1c Stabilize background WebView retention 2026-07-04 15:45:01 +00:00
harukin-expo-dev-env 9ea9d6764b Adjust WebView remount behavior for background tabs 2026-07-03 10:24:41 +00:00
harukin-expo-dev-env cf3995fcbe factor code structure for improved readability and maintainability 2026-07-02 02:37:34 +00:00
harukin-expo-dev-env 983c453322 Merge commit 'c69585b257d234fbc640839c851ed3d57cfebd63' into develop 2026-07-01 12:10:45 +00:00
harukin-expo-dev-env c69585b257 インポート機能の作成、録画システムのポジション最適化 2026-06-29 16:11:42 +00:00
harukin-expo-dev-env c6d4fca881 Merge commit '9c6719050935e76e3618ece27b9b838b5efe05e8' into develop 2026-06-29 14:43:20 +00:00
harukin-expo-dev-env 9c67190509 Update signage view configuration 2026-06-29 14:42:44 +00:00
harukin-expo-dev-env 956b660b4f Add recording JSON import and export 2026-06-29 14:39:30 +00:00
harukin-expo-dev-env f056fe593a Sentryロギングシステムを追加し、アプリのエラートラッキングを強化 2026-06-29 06:08:29 +00:00
harukin-expo-dev-env ca8d3225d1 Merge commit '0ebc6bc153d9893c7d067dd31805d2b87e3a3e9b' into develop 2026-06-27 04:37:27 +00:00
harukin-expo-dev-env 0ebc6bc153 Merge commit 'fbd342d640a13972e92a7ed35c061d42ca9f5a75' into patch/6.x 2026-06-27 04:35:48 +00:00
harukin-expo-dev-env fbd342d640 要素のサイズ変更 2026-06-27 04:33:54 +00:00
harukin-expo-dev-env b6e6d2e809 Merge commit '9eff185351a96859f9fb06c08b073c7b3e5cefa3' into develop 2026-06-25 17:46:42 +00:00
harukin-expo-dev-env 9eff185351 7.0.7 release 2026-06-25 17:36:48 +00:00
harukin-expo-dev-env 10e9aece2b Merge commit 'a1b914485134d0c1d1226a45ca43cd35505eeeef' into patch/6.x 2026-06-25 17:35:52 +00:00
harukin-expo-dev-env a1b9144851 Merge commit '16a62aa3bad5279d05dab49160a31388e4f9aef5' into develop 2026-06-25 17:16:17 +00:00
harukin-expo-dev-env 16a62aa3ba 小見出しも並べられるように変更 2026-06-25 16:57:20 +00:00
harukin-expo-dev-env f8395d7270 Hubアイコンを権限餅専用に 2026-06-25 16:20:04 +00:00
harukin-expo-dev-env 5a0fb9f43c 団体臨時から臨時を削除 2026-06-25 16:01:31 +00:00
harukin-expo-dev-env 9d188198a9 Merge commit 'cb1d2665d811ff6cc2a75806654c1e9f823201e7' into develop 2026-06-25 14:27:38 +00:00
harukin-expo-dev-env cb1d2665d8 切り替えスイッチを設置 2026-06-25 14:27:24 +00:00
harukin-expo-dev-env 3bbdd9d0a6 レイアウト機能調整 2026-06-25 13:26:51 +00:00
harukin-expo-dev-env 159f91e55e Merge commit 'daf261b4f842a5fa050cde0643d86654ae45150a' into feature/train-info-edit 2026-06-23 17:12:37 +00:00
harukin-expo-dev-env daf261b4f8 Merge commit '50fa4de9196adbb86313acadbe403d9a64af585a' into develop 2026-06-23 17:12:31 +00:00
harukin-expo-dev-env 50fa4de919 アイコンステータスの表示改善 2026-06-23 17:11:28 +00:00
harukin-expo-dev-env ab2061b2bd unyohubアイコン対応 2026-06-23 16:40:44 +00:00
harukin-expo-dev-env 4dd0775640 feat: add userscript generator for JR Shikoku operation info
- Implemented a script to generate a Tampermonkey userscript from ndView.tsx.
- Extracted and modified the operation page script body to include necessary postMessage functions.
- Added metadata for the userscript including name, namespace, version, and match URL.
- Ensured compatibility with Tampermonkey's GM_download for image capture.
2026-06-20 06:07:39 +00:00
harukin-expo-dev-env 4f81431ccd feat: 列車情報のキャプチャ機能を追加し、スタイルを改善 2026-06-10 05:04:09 +00:00
harukin-expo-dev-env 6371787b47 Merge commit '0ced766608b43cd9244817a0732246ca8b9c5f38' into develop 2026-06-06 08:35:26 +00:00
harukin-expo-dev-env 0ced766608 fix: バージョンコードを7.0.6に更新 2026-06-05 12:48:19 +00:00
harukin-expo-dev-env 26b61cdbc9 Merge commit '0e6763d9545f03b1331fc60e050dfa346afc4153' into patch/6.x 2026-06-05 12:35:07 +00:00
harukin-expo-dev-env 0e6763d954 feat: えれサイトの列車情報をグループ化し、表示を改善 2026-06-05 12:31:54 +00:00
harukin-expo-dev-env 7ba9a750ea feat: 録画の削除機能を追加し、スワイプ操作での削除を実装 2026-06-01 12:11:00 +00:00
harukin-expo-dev-env f9b2786809 Merge commit '95f498e89418c9bb8c551987f0b3f1b693c05c1d' into develop 2026-05-20 15:29:09 +00:00
harukin-expo-dev-env 95f498e894 fix: マリンライナー編成順序の修正(elesite連携)
マリンライナー(3xxxM)の岡山〜高松間の駅シーケンスを
elesiteTrainOrder.ts と webViewInjectjavascript.ts に追加。
originData には JR 西日本区間の駅がないため、
MARINE_STATION_SEQUENCE としてインラインで定義した。

これにより heading_to:"unknown" のマリン系統でも
正しく進行方向を推論できるようになり、
連結順序が逆順になる問題を解消した。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 13:48:47 +00:00
harukin-expo-dev-env a88f7bac09 Merge commit '23ef404f7b8f728749024b74e1b8742d5b75d529' into develop 2026-05-20 06:18:08 +00:00
harukin-expo-dev-env 23ef404f7b fix: update version code to 7.0.5 in SettingTopPage component 2026-05-20 06:17:56 +00:00
harukin-expo-dev-env 709cbda835 feat: add button for paper timetable in FixedContentBottom component 2026-05-20 06:02:50 +00:00
harukin-expo-dev-env f2bb72e40d Merge commit '6650a8c9513d426701abeef65d35bae0f8d79bb3' into patch/6.x 2026-05-19 03:07:03 +00:00
harukin-expo-dev-env 6650a8c951 feat: add experimental diagram URL support and update related components 2026-05-19 03:06:45 +00:00
harukin-expo-dev-env d76daf8791 Merge commit '8d7005170c0a7b89587871aef84ebb55df473c04' into patch/6.x 2026-05-15 17:22:28 +00:00
harukin-expo-dev-env 8d7005170c Refactor Elesite sorting logic and introduce new utility functions
- Moved the Elesite sorting logic to a new module `elesiteTrainOrder.ts`.
- Created utility functions to infer heading direction and sort Elesite entries by train number.
- Updated `injectJavascriptData` to utilize the new sorting logic and handle station sequences.
- Refactored `useElesite` to use the new sorting utility for improved readability and maintainability.
2026-05-15 17:21:18 +00:00
harukin-expo-dev-env 37e147e3be Merge commit '26aa71866ae1c949f0c31c7899ef68c30d111a61' into develop 2026-05-05 10:43:58 +00:00
harukin-expo-dev-env 26aa71866a feat: add Koboke Observatory to tourist spots data 2026-05-05 09:10:34 +00:00
harukin-expo-dev-env 2274934731 fix: update version code to 7.0.4 2026-05-05 08:12:25 +00:00
harukin-expo-dev-env 6c674a3f38 Merge commit 'd72acabe9de6b17c5e2206bea9f69ddd2ae3aea1' into patch/6.x 2026-05-05 08:10:22 +00:00
harukin-expo-dev-env d72acabe9d Merge commit '3e0e4876bc64e73100b6c825d6218201577a6f9a' into develop 2026-05-05 08:09:57 +00:00
harukin-expo-dev-env 3e0e4876bc feat: purple reload button when mock API is active
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 08:06:55 +00:00
harukin-expo-dev-env 3ab0b332ec Merge commit '5e6ff42fe6d6e30b30ae96e13fcd598197ca5fd8' into develop 2026-05-05 08:04:56 +00:00
harukin-expo-dev-env 5e6ff42fe6 Add script to compile web inject JavaScript with minification and obfuscation options
- Introduced `compile-web-script.ts` for generating JavaScript files for userscripts.
- Supports options for minification and obfuscation.
- Generates base JS, minified JS, and obfuscated userscript.
- Configurable via command line arguments and environment variables.
2026-05-05 08:04:47 +00:00
harukin-expo-dev-env 4916edd498 fix: use /train-positions/current endpoint for mock polling
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 18:25:47 +00:00
harukin-expo-dev-env 0c582765db feat: poll mock API server for live train positions
- positionMasters: add fetchMockTrainPositions() targeting
  /train-positions/latest on the mock API server
- useTrainMenu: when mockApiFeatureEnabled and not recording/playing,
  poll /train-positions/latest every 15s and update mockTrainPositions
  (same interval as live mode); on mode switch updates immediately
- useCurrentTrain:
  - playing: use local recording data (no network call)
  - mock ON idle: fetch from mock API server (15s poll driven by useTrainMenu)
  - mock OFF: fetch from real backend as before
- Recording still forces mock OFF so real API data is captured

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 18:01:12 +00:00
harukin-expo-dev-env ff6a8c9518 feat: integrate position-masters API into mock system
- Add lib/mockApi/positionMasters.ts
  - fetchPositionMasters() from jr-shikoku-backend-mock-api-v1.haruk.in
  - buildPosLookup() / lookupPos() helpers
  - serializePosLookupForJs() for baking into injected scripts
- webviewXhrInterceptor: accept positionMasters in MockApiConfig,
  bake _POS_LOOKUP into injected JS, enrich Pos from PosNum on serve
- useTrainMenu: fetch position masters on mock-enable (and at startup
  if already enabled); expose positionMasters + lookupPosText()
- useCurrentTrain: fill Pos via lookupPosText when mapping mock data
- mockApi/index.ts: re-export positionMasters types and helpers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-03 17:46:13 +00:00
harukin-expo-dev-env c425c0917b Merge commit '7b7ac73169255ae569ab38756f14a139dd255b9c' into develop 2026-05-02 09:15:31 +00:00
harukin-expo-dev-env 7b7ac73169 feat: add detailed changelog for version update to 7.0.3, highlighting new features and improvements 2026-05-02 08:59:55 +00:00
harukin-expo-dev-env 8594e79c25 Merge commit '6ce2cad4e5428dd30c066053176c59784cbe6fc0' into develop 2026-05-02 03:01:15 +00:00
harukin-expo-dev-env 6ce2cad4e5 fix: update version code to 7.0.3 2026-05-02 02:59:29 +00:00
harukin-expo-dev-env 75795f31e9 Merge commit '35542339943b4da6c181d5b6cc586d720752ab13' into patch/6.x 2026-05-02 02:53:10 +00:00
harukin-expo-dev-env 3554233994 fix: update header colors in injected JavaScript for better visibility 2026-05-02 02:52:22 +00:00
harukin-expo-dev-env d71cc373c9 fix: sync WebView train display when playback frame changes
The XHR interceptor baked _MOCK_TRAIN at page load time, so seeking or
advancing a playback frame only changed React state but had no effect
on the already-loaded WebView page.

- webviewXhrInterceptor.ts:
  - Expose window.__jrsMockUpdateTrain(newData) from inside the IIFE so
    the _MOCK_TRAIN variable can be updated after page load
  - Add generateMockUpdateScript(trainPositions) helper: calls
    __jrsMockUpdateTrain with fresh data + GetDateTime, then calls
    window.setReload() to trigger the page's own redraw cycle

- WebView.tsx:
  - Import generateMockUpdateScript
  - Add useEffect watching mockTrainPositions (skips first mount since
    beforeContentLoaded already has correct data)
  - When mockApiFeatureEnabled && mockTrainPositions changes, inject the
    update script via webview.current.injectJavaScript()

Result: seeking, prev/next frame, and auto-advance during playback now
immediately update the train position display in the WebView.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 01:06:34 +00:00
harukin-expo-dev-env 3587f72434 feat: add RecordingStatusBar with elapsed timer and keep-awake
- RecordingStatusBar.tsx: shown during recorderState === 'recording'
  - absolute positioned red bar at top of map screen (zIndex 2000)
  - blinking REC dot (700ms interval) + REC label + MM:SS elapsed timer
  - snapshot count display (right side)
  - pointerEvents="none" so it doesn't block map interaction
  - activateKeepAwakeAsync while recording, deactivated on stop/unmount
    (same pattern as FixedPositionBox, tag = 'recording-status-bar')
- Apps.tsx: render <RecordingStatusBar /> alongside PlaybackTimeline

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:59:03 +00:00
harukin-expo-dev-env 4f4d3cad0a fix: crash on playback start + support multiple recordings
Crash fix (React Hooks violation):
- PlaybackTimeline.tsx: move ALL hooks (useRef for PanResponder, useCallback)
  to before the early return; use seekRef/totalRef to share values into
  PanResponder handlers without stale closure issues

Multiple recordings support:
- trainRecorder.ts: redesign to id-based multi-recording system
  - RecordingMeta type for lightweight list (no snapshots)
  - TrainRecording now includes id field
  - saveRecording/loadRecordingById/deleteRecordingById/loadRecordingList
  - migrateOldRecording() migrates old single MOCK_RECORDING key on first launch
- constants/storage.ts: add MOCK_RECORDINGS_INDEX + MOCK_RECORDING_DATA_PREFIX keys
- useTrainMenu: savedRecording → recordingList (RecordingMeta[]) + activeRecording (TrainRecording|null)
  - startPlayback(id) loads full recording on demand
  - deleteRecording(id) deletes by id and refreshes list
  - stopPlayback clears activeRecording
- DataSourceSettings: recording list UI
  - shows all recordings with date/time, snapshot count, duration
  - ▶ play and 削除 buttons per row
  - recording/playing status indicator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:52:30 +00:00
harukin-expo-dev-env 8144e8a48a feat: add playback timeline UI with pause/resume/seek controls
- useTrainMenu: add playbackPaused state, pausePlayback/resumePlayback/seekToSnapshot
  - playback loop now respects playbackPaused (no timer when paused)
  - seekToSnapshot pauses playback and immediately applies snapshot data
  - expose playbackIndex, playbackPaused + new functions in context value
- PlaybackTimeline.tsx: new component shown when recorderState === 'playing'
  - absolute positioned bar at top of map screen (zIndex 2000)
  - prev/play-pause/next frame buttons + skip-to-start/end
  - time display (HH:mm:ss) + snapshot counter (n/total) + total duration
  - PanResponder-based scrubber track with filled progress bar and draggable thumb
- Apps.tsx: render <PlaybackTimeline /> alongside FixedPositionBox
- Remove MockApiToggle.tsx (no longer used since settings-only toggle)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:42:56 +00:00
harukin-expo-dev-env 37c08ad257 feat: add train position record & playback feature
Implements snapshot-based recording and playback of live train
position data for mock API debugging (admin-only).

Recording:
- startRecording() disables mock mode and begins capturing live
  train snapshots every ~15s (via getCurrentTrain polling)
- Each snapshot stores { t: elapsed_ms, trains: TrainEntry[] }
- stopRecording() saves completed recording to AsyncStorage

Playback:
- startPlayback() enables mock mode and loops through snapshots
  at their original recorded timing using useEffect+setTimeout
- stopPlayback() returns to idle

Storage:
- Single slot in AsyncStorage (MOCK_RECORDING key)
- Loaded on app start in useTrainMenu
- deleteRecording() removes it

New files:
- lib/mockApi/trainRecorder.ts — TrainRecording/TrainSnapshot types
  + save/load/delete AsyncStorage helpers

Modified files:
- constants/storage.ts — add MOCK_RECORDING key
- stateBox/useTrainMenu.tsx — recorder state + all controls
- stateBox/useCurrentTrain.tsx — call addTrainSnapshot after live fetch
- components/Settings/DataSourceSettings.tsx — record/play UI with
  status dot, snapshot count, and action buttons

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 00:31:55 +00:00
harukin-expo-dev-env b5eb830734 fix: update useWebViewRemount to include backgroundThresholdMs option for better app state handling 2026-05-01 23:25:50 +00:00
harukin-expo-dev-env 92f4b37861 feat: apply mock data to currentTrain (app-side train positions)
When MOCK_API_FEATURE_ENABLED is on, getCurrentTrain() now returns
mock data instead of fetching from the n8n webhook or fallback API.

- App.tsx: move TrainMenuProvider before CurrentTrainProvider so
  useTrainMenu() is available inside CurrentTrainProvider
- useCurrentTrain: import useTrainMenu + MOCK_TRAIN_POSITIONS;
  getCurrentTrain() short-circuits to mock data when mockApiFeatureEnabled;
  added useEffect on mockApiFeatureEnabled to reload immediately on toggle

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 12:40:30 +00:00
harukin-expo-dev-env a35956848a refactor: remove map-screen MOCK switch; settings toggle controls mock directly
The separate runtime MOCK switch on the map screen is removed.
Now the admin-only settings toggle (MOCK_API_FEATURE_ENABLED) is the
single control point — turning it on activates mock mode immediately,
turning it off deactivates it.

- useTrainMenu: remove mockApiEnabled/setMockApiEnabled state;
  mockApiConfig now derives from mockApiFeatureEnabled alone
- WebView: use mockApiFeatureEnabled for key prop (triggers reload)
- Apps.tsx: remove MockApiToggle import and JSX usage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 12:35:36 +00:00
harukin-expo-dev-env a809d287a2 fix: eliminate UX flash by calling setStrings() immediately in observer
The 100ms pure-debounce approach caused a visible layout flash on every
render (normal mode too) because Tokyo UX was not applied for 100ms.

Fix: call setStrings() immediately in the MutationObserver callback to
prevent any flash, AND keep a 250ms delayed follow-up call to catch
async train re-renders from mock XHR setTimeout(0) responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 11:59:52 +00:00
harukin-expo-dev-env 7566910821 fix: re-apply Tokyo UX after async train re-renders
Root cause: setStrings() was called synchronously in setReload(),
but the mock XHR interceptor responds via setTimeout(0), so the
site re-rendered train elements AFTER setStrings() ran, stripping
Tokyo UX styling.

Two-pronged fix:
1. setReload() now calls setStrings() again after 200ms to
   re-apply Tokyo UX once the async re-render completes.
2. textInsert MutationObserver now watches with subtree:true
   (childList only, not attributes - no infinite loop risk)
   with 100ms debouncing, so ANY train element re-render
   (including those triggered by mock XHR data) will trigger
   a re-application of Tokyo UX.
   - currentLines postMessage now only fires when direct
     children of #disp change (line selection changes only)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 11:51:13 +00:00
harukin-expo-dev-env 2cf6b679c5 fix: Tokyo UX stripped when mock is active
Two bugs caused the Tokyo UX to be stripped/broken when mock mode was ON:

1. **Double callback firing**: the interceptor's `setTimeout` called both
   `onreadystatechange.call(self)` AND `dispatchEvent(new Event('readystatechange'))`.
   Since `dispatchEvent` already fires `onXxx` property handlers via the DOM event
   model, the page's callback fired twice, causing a second DOM re-render that could
   overwrite Tokyo UX modifications. Fixed by relying on `dispatchEvent` only (with
   `ProgressEvent` for load events), with a direct-call fallback only for environments
   that lack `dispatchEvent`.

2. **No double-injection guard**: if `injectedJavaScriptBeforeContentLoaded` caused
   the interceptor to run more than once (e.g., `onPageStarted` fires multiple times
   on Android), `_origOpen` would capture the already-patched version, leading to
   unexpected behaviour. Fixed by adding `if (window.__jrsMockActive) return;` at the
   top of the interceptor IIFE.

3. **Belt-and-suspenders injection**: the interceptor is now also prepended to
   `injectedJavaScript` (Tokyo UX script) via the restored `mockApiConfig` parameter
   on `injectJavascriptData`. The `__jrsMockActive` guard ensures it's a no-op when
   `injectedJavaScriptBeforeContentLoaded` already ran it, but guarantees the
   interceptor is active on platforms where IJBCL is unreliable — before
   `MoveDisplayStation` triggers the first train-data poll via `onLoadEnd`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 11:40:11 +00:00
harukin-expo-dev-env 1dcc25dec0 chore: update train.json with live disrupted data (2026-05-01 19:42)
94列車、62列車遅延の運行乱れデータをキャプチャ。
モックAPIのテストデータとして有用な状態。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-01 10:43:19 +00:00
harukin-expo-dev-env 24f0c82b54 fix: call _origOpen even when intercepting to allow setRequestHeader
インターセプト対象のXHRでも_origOpen()を呼ぶよう修正。
これによりXHRがOPENED状態に入り、ページ側のsetRequestHeader()呼び出しが
エラーなく実行される。実際のリクエストはsend()でブロックすることで
モックデータのみ返す動作は維持される。

Playwright実機テスト結果:
- arg1=train&arg2=train: 2回インターセプト成功、実ネットワーク0件
- setRequestHeaderエラー: 解消
- 路線選択後のstation dataロード: 正常動作

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 22:49:22 +00:00
harukin-expo-dev-env 8321a47cbb fix: move XHR interceptor to injectedJavaScriptBeforeContentLoaded
インターセプターをページスクリプトより前に実行されるよう修正。
injectedJavaScript(ページ読込後)からinjectedJavaScriptBeforeContentLoaded
(ページスクリプト実行前)へ移動することで、モックON時のページフリーズを解消。

- lib/webViewInjectjavascript.ts: injectJavascriptDataからインターセプターを分離、
  generateBeforeContentLoadedScript()を新規エクスポート
- stateBox/useTrainMenu.tsx: injectJavascriptBeforeContentLoadedを別途計算しコンテキストに追加
- components/Apps/WebView.tsx: injectedJavaScriptBeforeContentLoadedプロップを追加

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 16:50:55 +00:00
harukin-expo-dev-env 59821a40d2 fix: rewrite XHR interceptor using prototype patching
Replaces the MockXHR wrapper class with direct prototype patching.

Root cause of broken page:
1. CRITICAL: callbacks called without .call(self) → 'this' inside
   onload/onreadystatechange was the global object, so this.responseText
   returned undefined and the page's JSON.parse crashed silently
2. responseType='json' not handled → page received string instead of
   parsed object when using xhr.response
3. instanceof XMLHttpRequest returned false, potentially breaking page
   validation logic

New approach (prototype-patch):
- Save original XMLHttpRequest.prototype.open and .send
- Patch open() to detect mock URL and store mock body on the instance
- Patch send() to define instance-level property getters (status,
  responseText, response, readyState) and fire callbacks with correct
  'this' context via .call(self)
- responseType='json' handled: response getter returns parsed object
- instanceof XMLHttpRequest always true (prototype chain untouched)
- dispatchEvent fires load/readystatechange for addEventListener users
- Non-mock requests delegate to original open/send unchanged

All 6 test cases pass: onload, onreadystatechange, addEventListener,
responseType=json, instanceof, and non-mock pass-through

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 16:05:41 +00:00
harukin-expo-dev-env 42473189da fix: reload WebView on mock toggle and fix XHR callback timing bug
- components/Apps/WebView.tsx:
  - import mockApiEnabled from context
  - add mockApiEnabled to WebView key so toggling forces a full reload
    (injectedJavaScript only runs at page load, so reload is required)

- lib/mockApi/webviewXhrInterceptor.ts:
  - Fix callback timing bug: callbacks set before open() were stored on
    _orig (because _mockBody was null), never on self._onload, causing
    mock send() to fire no-op callbacks
  - Always store all callbacks/listeners in _callbacks/_listeners maps
  - Apply _callbacks to _orig at send() time when not mocked
  - Fix addEventListener: always buffer in _listeners, apply to _orig
    at send() time; add removeEventListener support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 15:50:37 +00:00
harukin-expo-dev-env 71e1ad8d22 feat: add admin mock API toggle in settings and map screen switch
- constants/storage.ts: add MOCK_API_FEATURE_ENABLED key
- stateBox/useTrainMenu.tsx:
  - import MOCK_TRAIN_POSITIONS; auto-populate on mount and on toggle-off
  - add mockApiFeatureEnabled (persistent, admin-only) state + setter
  - mockApiConfig now requires both mockApiFeatureEnabled AND mockApiEnabled
- components/Settings/DataSourceSettings.tsx:
  - add mock API debug section (admin-gated, showDebugSelector)
  - Switch toggles mockApiFeatureEnabled and persists to AsyncStorage
- components/Apps/MockApiToggle.tsx: new component
  - absolute-positioned to the left of ReloadButton
  - visible only when mockApiFeatureEnabled && mapSwitch==="true"
  - MOCK label + Switch toggles mockApiEnabled runtime state
- components/Apps.tsx: render MockApiToggle alongside ReloadButton

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 15:41:24 +00:00
harukin-expo-dev-env 170fbf0a57 feat: add WebView XHR interceptor for mock train position injection
- Scrape & analyse all 19 internal APIs of train.jr-shikoku.co.jp/sp.html
  using puppeteer + Chromium remote debugging
- Save live-captured sample JSON for every API endpoint under
  lib/mockApi/mockData/ (station lists, lang, timetable, train positions, etc.)
- Add lib/mockApi/webviewXhrInterceptor.ts – generates a JS snippet that
  overrides XMLHttpRequest inside the WebView before page scripts run;
  intercepts /g?arg1=train&arg2=train (and optionally all static APIs)
  returning mock data instead of the real server response
- Add lib/mockApi/index.ts – convenience re-exports + MOCK_TRAIN_POSITIONS
  constant pre-populated with captured sample data
- Extend InjectJavascriptOptions with optional mockApiConfig field
- injectJavascriptData() prepends the interceptor JS when mockApiConfig is set
- TrainMenuContext gains mockApiEnabled / setMockApiEnabled and
  mockTrainPositions / setMockTrainPositions state; consumers can enable
  mock mode and inject custom TrainEntry[] without reloading the app

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-30 15:30:06 +00:00
harukin-expo-dev-env 899c655d8c fix: add platformNum property to train data structures for improved display 2026-04-30 15:18:28 +00:00
harukin-expo-dev-env 4c1a315f5d fix: add loading and error handling in GeneralWebView component with reload functionality 2026-04-30 14:27:15 +00:00
harukin-expo-dev-env a8c785bf7f fix: expose setReload function to the global window object for accessibility 2026-04-30 14:01:55 +00:00
harukin-expo-dev-env a9668e6d51 fix: trim train numbers in sorting and filtering logic for accurate matching 2026-04-30 09:49:44 +00:00
harukin-expo-dev-env 3a4e083b9d feat: add FormationChips, FadingSubCycler, and ActiveFormationChipsCycler components for enhanced train data visualization 2026-04-30 08:59:46 +00:00
harukin-expo-dev-env 45cc68ae56 fix: normalize train numbers by stripping suffixes in TrainDataSources and useUnyohub
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 15:51:15 +00:00
harukin-expo-dev-env d2e2f1d05c Merge commit '43f8095912f564ab02c3ddfbc0e218df5778cee6' into develop 2026-04-29 11:15:58 +00:00
harukin-expo-dev-env 43f8095912 feat: add changelog for version update from 064d81d4 to a959cf39, highlighting improvements in elesite integration, navigation stability, keyboard handling, and overall user experience 2026-04-29 11:15:52 +00:00
harukin-expo-dev-env 14e8e3422c Merge commit 'fac89f6f2ab74695e4f52e6a043e43f7f5c7f411' into patch/6.x 2026-04-26 08:49:28 +00:00
harukin-expo-dev-env fac89f6f2a fix: simplify elesite permission handling and update version code to 7.0.2 2026-04-26 08:43:04 +00:00
harukin-expo-dev-env 331b9d64d2 Merge commit '94eb84b6de6c0ad86f90542c80efe262595e8a57' into develop 2026-04-26 07:56:56 +00:00
harukin-expo-dev-env 94eb84b6de fix: enhance ListViewItem cycling animation and update train pair mapping in BusAndTrainDataProvider 2026-04-25 07:34:10 +00:00
harukin-expo-dev-env 9fb591d6c2 Merge commit '3ecb301e8276ca20ce9af849e5d808850653a0ab' into fix/April-Mid-Patch 2026-04-19 11:09:20 +00:00
harukin-expo-dev-env 3ecb301e82 fix: update WebView navigation and adjust interval timing in CurrentTrainProvider 2026-04-19 11:09:13 +00:00
harukin-expo-dev-env 16e06573a1 Merge commit '5a1430d84960b4e35b562c06c07ed13d3be09c79' into experiment/web-url-experimental 2026-04-14 13:55:43 +00:00
harukin-expo-dev-env 5a1430d849 fix(HeaderText): update todayOperation prop to filter out completed operations 2026-04-14 13:55:37 +00:00
harukin-expo-dev-env 0fa3ba6029 Merge commit 'ff908414116aeb75e5d735ca184c21c1a7d17ad3' into experiment/web-url-experimental 2026-04-13 23:54:29 +00:00
harukin-expo-dev-env ff90841411 Add new logo image for Elesite to relationLogo assets 2026-04-13 07:47:25 +00:00
harukin-expo-dev-env 2d14758e83 Merge commit 'a09ba456993d9f0854aa48beb8805a6fff4c0f96' into experiment/web-url-experimental 2026-04-12 11:18:09 +00:00
harukin-expo-dev-env a09ba45699 fix(ExGridView): remove zoom scale properties from Animated.ScrollView 2026-04-12 10:02:00 +00:00
harukin-expo-dev-env b3cf1c8ca3 Merge commit '76a617cde632ebca36da29a3a0f402541cc77191' into experiment/web-url-experimental 2026-04-12 08:19:06 +00:00
harukin-expo-dev-env 76a617cde6 fix(TrainDataView): update onLongPress condition to check currentTrainData instead of onLine 2026-04-12 08:18:59 +00:00
harukin-expo-dev-env 07399f4b4e fix(HeaderText): update todayOperation to use allTodayOperation for accurate state filtering
fix(TrainIconStatus): add cache option to fetch request for improved data handling
2026-04-12 08:18:33 +00:00
harukin-expo-dev-env 1b2ba087d5 feat: 投稿システム接続先のデバッグ機能を追加し、環境設定を管理できるようにした 2026-04-11 04:13:45 +00:00
harukin-expo-dev-env 374901c9fa fix: タブバーのアニメーションとキーボード非表示設定を削除
fix: JRSTraInfoコンポーネントの初期データ読み込み処理をuseEffectで追加
2026-04-10 09:36:09 +00:00
harukin-expo-dev-env 36be7801f6 fix(SearchUnitBox): use animatedOffset with Animated.View for smooth keyboard avoidance
- Replace measuredOffset (plain number) with animatedOffset (Animated.Value)
  so the search bar smoothly follows the keyboard instead of jumping abruptly
- Wrap position:absolute container in Animated.View to accept Animated.Value as bottom
- Remove LayoutAnimation.configureNext calls that conflicted with Animated.timing
  from useKeyboardAvoid, causing layout animation races on Android
- Drop unused keyboardHeight guard (keyboardHeight > 0 ? measuredBottom : 0);
  animatedOffset starts at 0 and is driven by the hook's timing, so no jump

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 02:30:46 +00:00
harukin-expo-dev-env b87c6f8f71 docs: キーボードアニメーション調整に関するドキュメントを追加 2026-04-09 10:46:54 +00:00
harukin-expo-dev-env 4017f82b10 fix: キーボード回避をAnimated.timing/springに移行し、高速切替時の位置ずれとアニメーション不動を解消
- LayoutAnimation.configureNext → Animated.timing/spring に全面移行
  - iOS: Animated.spring でキーボードアニメーションに追従
  - Android: Animated.timing + Easing.out(cubic) で自然な減速カーブ
- measureGenRef 世代カウンタで飛行中の古い measure() コールバックを無効化
- retryTimerRef (500ms) で adjustResize の中間座標を自動訂正
- currentAnimRef で高速切替時に前アニメをキャンセル
- AllTrainDiagramView / StationDiagramView を Animated.View 化
- docs: 改修資料を追加
2026-04-09 10:41:19 +00:00
harukin-expo-dev-env 58ce5fa2b5 feat: チュートリアル機能の設計案を追加し、ユーザーの初回体験を改善 2026-04-08 05:20:52 +00:00
harukin-expo-dev-env 4809426632 docs: ActionSheetアニメーション破綻の修正記録を追加 2026-04-08 05:03:19 +00:00
harukin-expo-dev-env 8b42644548 fix: EachTrainInfo ActionSheetのスプリングアニメーション破綻を修正
iOS (isModal=true) でマリンライナー等の走行中列車を表示した際に
ActionSheet のスライドアップアニメーションが瞬間表示になる問題を修正。

【根本原因】
1. iOS onOpen の発火タイミング問題(最重要)
   - ライブラリ内で onOpen が Modal.onShow にバインドされており、
     スプリングアニメーション開始「前」に発火する
   - onOpen 後に showThrew=true になると通過駅が追加されて高さが増加し
     onSheetLayout が再発火 → スプリングがほぼ終点からリスタート

2. useEffect による非同期な高さ変化
   - useThroughStations / useStopStationIDs / useTrainDiagramData が
     useState([]) で初期化し useEffect で計算していたため
     空リスト → フルリストの高さ変化が onSheetLayout をトリガーしていた

3. useAutoScroll の InteractionManager が Reanimated アニメーションを認識しない

【修正内容】
- EachTrainInfoCore: showThrew の初期値を useState(() => !!getCurrentStationData(...))
  に変更し、走行中なら最初から true にして高さ変化を防ぐ
- useTrainDiagramData / useThroughStations / useStopStationIDs:
  純粋計算関数を抽出し useState lazy initializer で初回レンダリング時から正確な高さを確保
- EachTrainInfo: onOpen/onClose で sheetOpened state を管理し EachTrainInfoCore に渡す
- useAutoScroll: setShowThrew 引数を削除、sheetOpened フラグでスクロールをゲート
2026-04-08 05:00:58 +00:00
harukin-expo-dev-env 5914646443 stackAwareNavigate関数を導入し、遷移時のナビゲーションロジックを改善 2026-04-08 02:54:30 +00:00
harukin-expo-dev-env a54ef7ca13 ナビゲーションロジックを改善し、stackAwareNavigate関数を導入して遷移時のスタック管理を強化。プライバシーポリシーと設計メモを追加。 2026-04-08 02:54:14 +00:00
harukin-expo-dev-env 6b46c7150c AppContainerのonStateChangeロジックを改善し、アクティブなルートの状態を正確にチェックするように修正 2026-04-05 06:07:17 +00:00
harukin-expo-dev-env 9e2abc96c7 StatusBarの表示ロジックを改善し、Appsコンポーネントにフォーカス状態を追加 2026-04-05 06:07:04 +00:00
harukin-expo-dev-env ad5357ce7f 運用Hub情報の取得ロジックを改善し、貨物列車の車番処理を追加 2026-04-03 02:07:26 +00:00
harukin-expo-dev-env 045ed21cd7 噂機能のスタイル強化 2026-04-02 15:25:22 +00:00
harukin-expo-dev-env a9fef20f78 Merge commit '0764c17d43dedad48f74173baec555dd95c44b4c' into develop 2026-04-02 15:24:27 +00:00
harukin-expo-dev-env 0764c17d43 MapPinコンポーネントにおけるマーカーの表示方法を改善し、画像をViewでラップしてスタイルを適用 2026-04-01 11:13:54 +00:00
harukin-expo-dev-env b12269d35d バージョンコードを7.0から7.0.1に更新 2026-04-01 10:23:36 +00:00
harukin-expo-dev-env 057e595220 各コンポーネントにおけるLive Activity機能の一時的無効化、アイコン設定の改善、アイコンリストのセクション化、及び関連する状態管理の追加 2026-04-01 10:14:49 +00:00
harukin-expo-dev-env 2be3a5c481 アセット画像と変更履歴を追加 2026-04-01 10:08:10 +00:00
harukin-expo-dev-env a3873f55be Merge commit 'dbfbc43316527f21f956922fcc609f17b3581e1d' into patch/6.x 2026-03-31 17:42:19 +00:00
harukin-expo-dev-env dbfbc43316 iOSビルド用設定変更 2026-03-31 17:39:24 +00:00
harukin-expo-dev-env 69e61b401c Live Activity機能を一時的に無効化し、関連するコードを修正 2026-03-31 17:34:59 +00:00
harukin-expo-dev-env bed4366654 iOSビルド番号を61から62に更新 2026-03-31 15:01:59 +00:00
harukin-expo-dev-env 0be77a56ae Merge commit '131bd309842dee49ce3606cf7c371b7f223d61ee' into develop 2026-03-31 15:01:27 +00:00
harukin-expo-dev-env 131bd30984 ウィジェットのテーマカラーを追加し、各ウィジェットでの色の適用を改善 2026-03-31 14:59:47 +00:00
harukin-expo-dev-env af4e0d9438 SettingTopPageとSoundSettingsのリファクタリング、不要なコードの削除とオーディオプレイヤー機能の追加 2026-03-31 11:17:19 +00:00
harukin-expo-dev-env ec9b6dd1bc アイコン設定画面のリファクタリングとローディングアニメーションの追加 2026-03-31 06:56:35 +00:00
harukin-expo-dev-env e1c12c9dab アプリアイコンシステムガイドを追加し、アイコンの追加手順と規格を明確化 2026-03-31 03:32:49 +00:00
harukinMBP ad75dfe27c iOSウィジェットにURLを追加し、ユーザーがトレイン情報と運行情報にアクセスできるようにしました 2026-03-31 12:10:07 +09:00
harukin-expo-dev-env 9c926362a9 アイコンのIDの大文字小文字を統一し、可読性を向上 2026-03-31 02:20:44 +00:00
harukin-expo-dev-env 0a1940f781 テキストスタイルにテーマカラーを適用し、可読性を向上 2026-03-31 02:15:14 +00:00
harukin-expo-dev-env f937bcc22d Add new PNG icons for various assets
- Introduced multiple new PNG icons in the assets/icons directory, including:
  - s40.png
  - s54nany1.png
  - s54nany2.png
  - s54s.png
  - s54to0ys.png
  - s6000f.png
  - s7001.png
  - s8001nr.png
  - s9000.png
  - tosa9640ht.png
  - tosa9640jbl.png
  - tosa9640mo1.png
  - tosa9640mo2.png
  - tosa9640tyb.png
  - tosa9640tyg.png
  - w141jg.png
  - w40.png
  - w741.png

These icons are essential for enhancing the visual representation of the application.
2026-03-31 01:50:19 +00:00
harukin-expo-dev-env 69f2d38a0a useThemeColorsを使用してダークモードの判定を統一し、コードの可読性を向上 2026-03-31 00:21:38 +00:00
harukin-expo-dev-env 1d9a1d593b useThemeColorsからisDarkを追加し、ユーザーインターフェーススタイルを動的に設定 2026-03-31 00:01:07 +00:00
harukin-expo-dev-env b3d7ba448d WebViewコンポーネントにcontentMode="mobile"を追加し、表示を最適化 2026-03-30 23:45:31 +00:00
harukin-expo-dev-env 3cb14405f6 画面遷移アニメーション完了後に重たい計算を実行するためのフラグを追加し、ローディングアニメーションを実装 2026-03-30 12:51:12 +00:00
harukin-expo-dev-env 33defa1182 TextInputコンポーネントのプレースホルダーのテキストカラーをcolors.textTertiaryに変更 2026-03-30 11:07:23 +00:00
harukin-expo-dev-env 1084b6b299 各コンポーネントのアニメーション最適化とリファクタリング: useSharedValueとuseAnimatedStyleを導入し、パフォーマンスを向上 2026-03-30 10:11:14 +00:00
harukin-expo-dev-env fbb8580d28 ActionSheetアニメーション阻害の修正: LayoutAnimation削除とブリンクアニメーション最適化
- ActionSheet内外のLayoutAnimation.configureNext/easeInEaseOutを削除
  (グローバルにレイアウト更新を乗っ取り、シートの開閉アニメーションと競合するため)
- trainIconStatus: reanimated withRepeat→useInterval+setStateに変更
  (UIスレッドの無限アニメーションがActionSheetスプリングと競合するため)
- EachTrainInfo: Rules of Hooks違反修正(useRef/useSheetMaxHeightを条件分岐前に移動)
- useAutoScroll: InteractionManager.runAfterInteractionsでシート展開完了後に実行

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 10:09:45 +00:00
harukin-expo-dev-env 5db74714db TrainDataSources: 列番上書き時にヘッダーでくるくるアニメーション表示 2026-03-30 10:00:20 +00:00
harukin-expo-dev-env 5a106a3ab7 アイコンのリファクタリング: hub_logo.pngとicon_2048.webpを削除し、elesite_logo.pngとunyohub_logo.webpを追加 2026-03-30 07:21:07 +00:00
harukin-expo-dev-env 0ef169518a HeaderText: useMemoフックにtrainNumとallCustomTrainDataを追加して依存関係を最適化
TrainIconStatus: アンパンマンステータスAPIのエンドポイント判定を追加
webViewInjectjavascript: 新しい列車アイテムの最大幅を設定
2026-03-30 05:54:25 +00:00
harukin-expo-dev-env 9d76264a28 各コンポーネントでのuseMemoフックの追加とデータ処理の最適化 2026-03-30 04:00:25 +00:00
harukin-expo-dev-env 1181e488c1 Merge commit 'bbfd82aaea9fa2460e97ac8eee4d9beb7c9f8034' into develop 2026-03-30 02:27:24 +00:00
harukin-expo-dev-env bbfd82aaea EachTrainInfo: 内部ScrollViewのmaxHeightを80%→70%に変更 2026-03-30 02:18:10 +00:00
harukin-expo-dev-env bd10290bb3 ActionSheet: スマホのmaxHeightを90%→70%に変更 2026-03-30 02:12:07 +00:00
harukin-expo-dev-env 63431adab1 ActionSheet: スマホのみmaxHeight(90%)制限、タブレットは無制限
- useSheetMaxHeight() hook追加(短辺<600dpでスマホ判定)
- 全8つのActionSheetにmaxHeight適用
- タブレット/DeXでは制限なし
2026-03-30 02:03:57 +00:00
harukin-expo-dev-env 393bcc4df3 DeX: Dimensions.getモンキーパッチ + transform scaleで低密度ディスプレイ対応
- Dimensions.get('window')を1/1.3に縮小パッチ
- useWindowDimensions()も自動的に小さい値を返す
- DensityScaleWrapperで1.3倍transform scaleで拡大
- コンポーネントは872x531dpとしてレイアウト→実画面1133x690に拡大
2026-03-29 23:31:26 +00:00
harukin-expo-dev-env ec53d4fa2a DeX: DensityScaleWrapperを一旦削除、正常表示に戻す 2026-03-29 17:32:17 +00:00
harukin-expo-dev-env dd62ad8f73 DeX: スケール値を1.8→1.3に低減、レイアウト崩れ修正 2026-03-29 17:29:06 +00:00
harukin-expo-dev-env 25e13b9f41 DeX: DensityScaleWrapperでtransform scaleアプローチに切替、babel plugin wrapStyleForDensityを除去 2026-03-29 17:25:54 +00:00
harukin-expo-dev-env b9f8ed1ea8 test: compensation倍率を大幅増加(2.625/PR, max4.0)で効果確認テスト 2026-03-29 17:16:21 +00:00
harukin-expo-dev-env 2b4b237d1a fix: babelプラグインでText/TextInputのstyleをglobal.__scaleTextStyleでラップ
- Text.renderが存在しない(関数コンポーネント)ためモンキーパッチ不可
- babel pluginでビルド時に全Text/TextInputのstyleプロップをラップ
- ランタイムのglobal.__scaleTextStyleがrender毎にPR動的チェック
- PR<2.0: fontSize * min(2.5, 2.0/PR)で拡大
- PR>=2.0: 変更なし(identity)
2026-03-29 17:10:59 +00:00
harukin-expo-dev-env a69b59ed84 debug: Text構造の診断Alert追加(render存在確認) 2026-03-29 17:02:05 +00:00
harukin-expo-dev-env 49c69ffe53 fix: PixelRatioをrender時に動的チェック(起動時はPhone PR、DeX移動後にPR低下するため) 2026-03-29 16:59:00 +00:00
harukin-expo-dev-env 040ce9bce1 fix: Text.renderモンキーパッチで全テキストを低密度ディスプレイで自動拡大
- utils/scaleTextForDensity.ts: PR<2.0のとき全TextのfontSizeを2.0/PR倍(max2.5x)
- App.tsx: 起動時にscaleTextForDensityをインポート
- responsive.ts: fontScaleをidentityに戻す(二重スケーリング防止)
- 178箇所のハードコードfontSizeも個別修正不要で一括対応
2026-03-29 16:56:17 +00:00
harukin-expo-dev-env 06b2e97392 fix: DeX(PR=0.756)実測値に基づく密度補償 fontScale=2.0/PR(max 2.5x)
- DeX実測: Win=1133x690, PR=0.756, FS=0.8, Scr=731x411
- fontScale: 2.0/0.756 = 2.64 → cap 2.5x(テキスト2.5倍拡大)
- moderateScale: 拡大なし(レイアウト寸法保護)
- デバッグAlert/DebugMetrics除去
2026-03-29 16:46:01 +00:00
harukin-expo-dev-env b8481e985e debug: Alert.alertでネイティブダイアログ表示(密度非依存の確実な方法) 2026-03-29 16:39:49 +00:00
harukin-expo-dev-env 146602caa1 debug: DebugMetricsをScrollView外に移動(常に画面上部に表示) 2026-03-29 16:35:50 +00:00
harukin-expo-dev-env c07f8f1677 debug: デバッグパネルを赤背景+fontSize40で無条件表示(DeX値確認用) 2026-03-29 16:32:35 +00:00
harukin-expo-dev-env 8a318475bf fix: 密度補償を大幅に縮小 - fontScaleのみ1.3x、moderateScaleは拡大なし
- moderateScaleがwidth/height/borderRadiusにも使われておりレイアウト崩壊の原因
- fontScale: 低密度時1.3倍(テキスト可読性のみ改善)
- moderateScale: 常に原寸(レイアウト保護)
- デバッグパネル: 低密度時fontSize固定24
2026-03-29 16:28:10 +00:00
harukin-expo-dev-env f5abf0d85a fix: DeX低密度ディスプレイでフォント/アイコンを拡大する密度補償を追加
- PixelRatio < 2.0 のとき densityCompensation = 2.0/PR で拡大(最大2.5倍)
- fontScale/moderateScale が密度補償倍率を適用
- デバッグパネルも密度に応じてfontSizeを自動拡大
- Phone窓モード(PR=2.01)では変化なし、DeX(PR≈1.0)では約2倍に拡大
2026-03-29 16:21:25 +00:00
harukin-expo-dev-env e373ffdb76 fix: DeXで文字の縮小を停止、デバッグパネルのフォントサイズを拡大
- fontScale/moderateScale を恒等関数に変更(低密度ディスプレイでの縮小を停止)
- verticalScale のみレイアウト高さ圧縮に使用
- デバッグパネルのフォントサイズを10→16に拡大(DeXでも読めるように)
- デバッグ情報を簡潔化(Win/Scr/LowDensity表示)
2026-03-29 16:14:34 +00:00
harukin-expo-dev-env 61074d0bfe debug: DeX環境調査用のメトリクス表示を追加(一時的)
useWindowDimensions, Dimensions(window/screen), PixelRatio, FontScaleの
実際の値をメイン画面に表示してDeXでの挙動を特定する
2026-03-29 16:03:33 +00:00
harukin-expo-dev-env 9a03143853 fix: レスポンシブスケーリングを大幅強化 - fontScale 0.4→0.8、moderateScale 0.5→0.8
- responsive.ts: スケーリングファクターを大幅強化(fontScale 0.8, moderateScale 0.8)
- initIcon: タブバーアイコンsize 30→moderateScale(30)
- SimpleDot: ドットサイズ20/14→moderateScale
- StationPagination: 全寸法(28/24/18/22/8/6)をmoderateScale/fontScale化
- CarouselBox: fontSize 20/14→fontScale
- MenuPage: マップオフセット100/80/10/30→verticalScale
- menu: スクロールオフセット/snapToOffsets→verticalScale
2026-03-29 15:52:29 +00:00
harukin-expo-dev-env 3ca109edee feat: メイン画面のレスポンシブ対応 - Menu/atom/TrainMenuコンポーネントにスケーリング適用
- TitleBar: ヘッダー高さをverticalScaleに
- UsefulBox: アイコン50→moderateScale、フォント16→fontScale
- TicketBox: フォント18→fontScale
- TextBox: minHeight 70→verticalScale
- FixedContentBottom: 全fontSize(20/18)→fontScale、全アイコンsize(50/40)→moderateScale
- JRSTraInfoBox: 全fontSize(30/20/18)、アイコン、maxHeight、ローディングサイズをスケーリング
- SpecialTrainInfoBox: fontSize(30/20)→fontScale
- CarouselTypeChanger: height 40→verticalScale
2026-03-29 15:41:06 +00:00
harukin-expo-dev-env 5420531c64 feat: Samsung DeXレスポンシブ対応 - lib/responsive.ts追加、主要コンポーネントにスケーリング適用
- useResponsive()フックを作成(fontScale/verticalScale/moderateScale)
- ベースデザイン: iPhone 14 Pro (393x852)、スケールダウンのみ(最大1.0)
- DynamicHeaderScrollView: ヘッダー高さにverticalScale適用
- EachTrainInfoCore: maxHeight緩和(70→80%)、ハンドラーサイズスケーリング
- StateBox/PositionBox/ScrollStickyContent/HeaderText: fontScale適用
- DataConnectedButton/EachStopList: fontScale+moderateScale適用
- JRSTraInfo/StationDeteilView: ハンドラー+フォントスケーリング
- ReloadButton/MapsButton/NewMenu: ボタンサイズ+フォントスケーリング
- useDeviceOrientationChange: 全Android端末でisLandscape=false
2026-03-29 15:23:28 +00:00
harukin-expo-dev-env 8116ecd197 fix: タブレットの判定ロジックを追加し、サイドパネルレイアウトの使用を制御 2026-03-29 14:27:10 +00:00
harukin-expo-dev-env 1ecd67d8c5 fix: iPad ActionSheet/isLandscape対応 - isModalをiPad無効化、maxHeight追加、isLandscape常時false 2026-03-29 14:09:19 +00:00
harukin-expo-dev-env 6dd24b36ec fix: iPadでのヘッダー表示問題を修正 (ScrollView→View, width: 100%) 2026-03-29 14:07:07 +00:00
harukin-expo-dev-env 84b043ff5f fix: FelicaBalanceWidgetの表示をiPhoneのみに制限し、ダーク/ライトモードに対応した色設定を追加 2026-03-29 11:55:12 +00:00
harukin-expo-dev-env 5cf864b9ab fix: サウンド設定のJavaScriptインジェクションを遅延させ、列車番号のオーバーライドを追加 2026-03-29 11:42:09 +00:00
harukin-expo-dev-env 43758aa781 fix: Unyohub列番の上書き処理を追加し、運用Hub情報の取得を改善 2026-03-29 11:38:18 +00:00
harukin-expo-dev-env 684a184d40 fix: サウンド設定とレイアウト設定のナビゲーションを修正し、視認性向上のためのセクションヘッダーを追加 2026-03-28 04:07:46 +00:00
harukin-expo-dev-env 5e66fab175 feat: カラーテーマ設定とサウンド設定機能を追加し、外部起動方法をREADMEに記載 2026-03-26 13:55:24 +00:00
harukin-expo-dev-env e1293d2500 fix: UIの色設定をダークモードに対応させ、視認性を向上 2026-03-26 08:56:31 +00:00
harukin-expo-dev-env 8ce0244c4b fix: 駅固定通知の出発済み列車フィルタと棒線駅接近判定を修正
- バックグラウンドで出発済み列車が消えないバグを修正
  (LiveActivityForegroundService: scheduledMinutes+delay<nowMinutes で除外)
- parseTimeToMinutes() ヘルパーを Kotlin に追加
- UI側も遅延加算した発車時刻で出発済み判定するよう統一
  (StationDiagramView: getDelayMinutes + dayjs.add)
- between.ts の BetweenStation を使った接近セクション判定を実装
  (棒線駅で列車がセクション手前に居る場合は未到達扱いで保持)
- StationTrainInfo に sectionStation フィールドを追加
  (prevStop/nextStop を廃止し API Pos と直接照合可能な文字列に変更)
- docs/station-lock-dual-impl.md 作成(KotlinとTS間のデュアル実装ガイド)
2026-03-26 03:00:59 +00:00
harukin-expo-dev-env 50e514543b Refactor code structure for improved readability and maintainability 2026-03-25 15:09:56 +00:00
harukin-expo-dev-env e93cc93b77 Merge commit 'bf5be9bd57360e98b4279c4b4630c2a49ecc8062' into develop 2026-03-25 14:06:43 +00:00
harukin-expo-dev-env bf5be9bd57 feat: implement sound announcement feature for delayed trains with settings 2026-03-25 14:05:59 +00:00
harukin-expo-dev-env 9e45d592b1 refactor: improve audio source handling for platform compatibility 2026-03-25 04:07:28 +00:00
harukin-expo-dev-env 834ad2bd41 feat: implement elesite data source integration and permission handling 2026-03-25 01:56:12 +00:00
harukin-expo-dev-env 83ca18f2c7 feat: add detailed changelog for version 7.0beta with feature enhancements and improvements 2026-03-25 01:44:54 +00:00
harukin-expo-dev-env baacfd5855 refactor: remove LiveActivityButton from EachTrainInfoCore component 2026-03-25 01:30:57 +00:00
harukinMBP 066317bbc8 feat: add SpotSign component for tourist attractions 2026-03-25 10:22:12 +09:00
harukinMBP 0bcb03f833 refactor: remove live notification functionality from FixedStation and FixedTrain components 2026-03-25 02:07:43 +09:00
harukinMBP 9037d21237 fix: TypeScript build errors (143 to 47) 2026-03-25 01:44:29 +09:00
harukin-expo-dev-env 4789543573 Merge origin/develop into develop and resolve conflicts 2026-03-24 16:11:08 +00:00
harukin-expo-dev-env 5345dca95f Merge branch 'feature/fix-some' into develop 2026-03-24 16:07:30 +00:00
harukin-expo-dev-env b1a8a4c98f feat: update iOS build number to 59 in app.json 2026-03-22 17:11:15 +00:00
harukin-expo-dev-env 50fd2ece64 Merge commit 'f972d2b719eb9d26a738761800bc1901c573c800' into develop 2026-03-22 17:10:00 +00:00
harukin-expo-dev-env f972d2b719 Merge commit 'deb24caaa2472ac2ec7715c96f3d0b69ebb96cde' into feature/migration-expo-upgrade 2026-03-22 17:09:51 +00:00
harukin-expo-dev-env deb24caaa2 feat: add station progress tracking and notification updates for train follow feature 2026-03-22 17:09:19 +00:00
harukinMBP 5515f42415 feat: add Live Activity button for train tracking and update iOS deployment target 2026-03-23 02:07:24 +09:00
harukin-expo-dev-env fbfb83fa34 feat: add live activity notifications for train tracking and station locking
- Implemented ExpoLiveActivity module for Android to manage live notifications.
- Added foreground service for train tracking and station locking notifications.
- Updated app permissions to include POST_NOTIFICATIONS.
- Enhanced FixedStation and FixedTrain components to support live notifications.
- Introduced new notification builders for train and station activities.
- Updated useCurrentTrain and useNotifications hooks to manage live notification state.
- Added notification channel for live tracking in Android.
2026-03-22 16:15:48 +00:00
harukinMBP 8eb49f57d6 fix: update activity state handling to use ActivityContent with nil staleDate 2026-03-22 23:09:13 +09:00
harukinMBP c5d4dc3b65 feat: add automatic Live Activity updates for train position and station lock in TrainDataView and StationDiagramView 2026-03-22 23:03:07 +09:00
harukinMBP 7f2480bc01 feat: add Live Activity support for station locking in StationDiagramView 2026-03-22 23:01:00 +09:00
harukinMBP b8372e5087 feat: add hooks for managing Live Activities for station locking and train following 2026-03-22 22:51:58 +09:00
harukinMBP 75c07f013d feat: add Live Activities support for train tracking and station locking 2026-03-22 22:49:05 +09:00
harukin-expo-dev-env 46bfea4e13 feat: add androidGoogleMapsApiKey to app.json configuration 2026-03-22 13:17:53 +00:00
harukin-expo-dev-env 5a2dc8c6a8 feat: add appleTeamId to iOS configuration in app.json 2026-03-22 11:48:59 +00:00
harukin-expo-dev-env 06650d014a feat(widget): add Shortcut, Delay Info, and Felica Balance widgets
- Implemented ShortcutWidget for quick access to app features with customizable shortcuts.
- Added DelayInfoWidget to display train delay information fetched from a remote endpoint.
- Created FelicaBalanceWidget to show the balance of Felica-compatible IC cards.
- Introduced OperationInfoWidget for displaying train operation status.
- Set up shared data handling for Felica snapshots between the main app and widget.
- Configured widget assets and entitlements for proper functionality.
- Updated Info.plist and expo-target.config.js for widget deployment.
2026-03-22 11:45:58 +00:00
harukin-expo-dev-env 45feeece58 feat: FeliCa対応の可用性チェック機能を追加 2026-03-22 10:13:36 +00:00
harukinMBP 385c2d8b86 Update app.json build number to 58 and refine NFC error handling in ExpoFelicaReaderModule 2026-03-22 18:56:13 +09:00
harukinMBP 41cc70086a Implement NFC scanning functionality in ExpoFelicaReader module 2026-03-21 22:04:55 +09:00
harukinMBP 98c71127aa Fix .gitignore: re-include modules/**/ios/ for EAS build, remove debug code 2026-03-21 20:13:34 +09:00
harukinMBP d049d3b07d Debug: patch resolveCommand.js and apple.js with logging, add pod fallback 2026-03-21 20:03:40 +09:00
harukinMBP 8484f15092 Enhanced autolinking debug: use external JS file 2026-03-21 19:46:25 +09:00
harukinMBP 1afa5e4377 Add Podfile debug plugin to trace autolinking resolve 2026-03-21 19:33:22 +09:00
harukinMBP 9f6d86b8b6 Fix ExpoFelicaReader module: restore full definition, update diagnostics 2026-03-21 17:54:54 +09:00
harukinMBP 72e7d63bd7 Add expo-felica-reader to dependencies for EAS build autolinking 2026-03-21 17:54:39 +09:00
harukin-expo-dev-env b243439e78 Refactor code structure for improved readability and maintainability 2026-03-20 07:14:58 +00:00
harukin-expo-dev-env 65f3b2a877 fix: ダークモード - 設定リスト・お気に入り並び替え・SNSリストの表示修正
- SettingTopPage: SettingList の containerStyle を直接設定して背景色を適用
- FavoiliteSettingsItem: 駅名テキストと並び替えアイコンに color: colors.text を追加
- SocialMenu: SNS一覧の ListItem に containerStyle と ListItem.Title の色を追加

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 08:46:14 +00:00
harukin-expo-dev-env 925d162f26 feat: iPad サポートを有効化
app.json の supportsTablet を true に変更

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 08:44:36 +00:00
harukin-expo-dev-env 731fe504c6 perf: dev client で expo-updates をスキップ
__DEV__ 時は checkForUpdateAsync() が不要なため早期リターン

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 08:38:10 +00:00
harukin-expo-dev-env 942ec395f1 perf: metro transformer に experimentalImportSupport を復元
lazyImports: true と組み合わせて動作するオプション。
resolver への誤設定は除外し transformer のみに適用。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 07:42:55 +00:00
harukin-expo-dev-env 306cf6882e chore: 不要なビルドファイル build-1773815539430.apk を削除 2026-03-18 07:12:45 +00:00
harukin-expo-dev-env 468bb4633a chore: metro.config.js から deprecated な experimentalImportSupport を削除
SDK 55 の Metro では不要。resolver への設定は誤り。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-18 07:11:15 +00:00
304 changed files with 32670 additions and 4617 deletions
+2 -1
View File
@@ -59,4 +59,5 @@ ios/
!modules/**/ios/
*.ipa
*.apk
*.aab
*.aab
.env.local
+7
View File
@@ -0,0 +1,7 @@
{
"mcpServers": {
"Sentry": {
"url": "https://mcp.sentry.dev/mcp/xprocess-m5/jr-shikoku-unofficial-apps"
}
}
}
+171 -33
View File
@@ -1,5 +1,11 @@
import React, { useEffect } from "react";
import { Linking, Platform, UIManager } from "react-native";
import {
IS_LOW_DENSITY,
DEX_SCALE,
useRealWindowDimensions,
} from "./utils/dexDimensionOverride";
import { Linking, Platform, UIManager, View } from "react-native";
import { enableFreeze, enableScreens } from "react-native-screens";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import "./utils/disableFontScaling"; // グローバルなフォントスケーリング無効化
import { AppContainer } from "./Apps";
@@ -20,22 +26,98 @@ import { buildProvidersTree } from "./lib/providerTreeProvider";
import { StationListProvider } from "./stateBox/useStationList";
import { NotificationProvider } from "./stateBox/useNotifications";
import { UserPositionProvider } from "./stateBox/useUserPosition";
import { rootNavigationRef } from "./lib/rootNavigation";
import {
markStartupExplicitTarget,
resolveStartupNavigationSource,
rootNavigationRef,
stackAwareNavigate,
} from "./lib/rootNavigation";
import { AppThemeProvider } from "./lib/theme";
import StatusbarDetect from "./StatusbarDetect";
import * as Sentry from '@sentry/react-native';
import {
startAppLifecycleCrashSentinel,
stopAppLifecycleCrashSentinel,
} from "./lib/observability/appLifecycleCrashSentinel";
Sentry.init({
dsn: 'https://1090312e4cf501f5a455d523eff2d538@o4511646874664960.ingest.us.sentry.io/4511646880432128',
// Adds more context data to events (IP address, cookies, user, etc.)
// For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/
sendDefaultPii: true,
// Enable Logs
enableLogs: true,
// Configure Session Replay
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1,
integrations: [Sentry.mobileReplayIntegration(), Sentry.feedbackIntegration()],
tracesSampleRate: __DEV__ ? 1.0 : 0.05,
beforeSend(event) {
const dataFetch = event.contexts?.data_fetch as any;
if (dataFetch?.responseHead && typeof dataFetch.responseHead === "string") {
dataFetch.responseHead = dataFetch.responseHead.slice(0, 300);
}
return event;
},
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: __DEV__,
});
LogBox.ignoreLogs([
"ViewPropTypes will be removed",
"ColorPropType will be removed",
]);
const nativeScreensDisabled =
Platform.OS === "ios" || Platform.OS === "android";
if (nativeScreensDisabled) {
enableFreeze(false);
enableScreens(false);
}
if (Platform.OS === "android") {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
export default function App() {
export default Sentry.wrap(function App() {
useEffect(() => {
const nativeScreensMode = nativeScreensDisabled
? "screens-disabled"
: "default";
const rootTabSceneMode =
Platform.OS === "android" ? "opacity-fixed-z-v1" : "default";
Sentry.setTag("native_screens_mode", nativeScreensMode);
Sentry.setTag("root_tab_scene_mode", rootTabSceneMode);
Sentry.setContext("runtime_navigation_flags", {
platform: Platform.OS,
nativeScreensMode,
rootTabSceneMode,
});
Sentry.addBreadcrumb({
category: "runtime.flags",
level: "info",
message: "runtime navigation flags applied",
data: {
platform: Platform.OS,
nativeScreensMode,
rootTabSceneMode,
},
});
void startAppLifecycleCrashSentinel({ nativeScreensMode });
return () => {
stopAppLifecycleCrashSentinel();
};
}, []);
useEffect(() => {
UpdateAsync();
}, []);
@@ -49,12 +131,12 @@ export default function App() {
return;
}
rootNavigationRef.navigate("topMenu", {
stackAwareNavigate("topMenu", {
screen: "setting",
params: {
screen: "FelicaHistoryPage",
},
} as any);
});
};
const navigateWhenReady = (
@@ -73,42 +155,69 @@ export default function App() {
const routeFromUrl = (url: string, retryCount = 0) => {
const normalized = (url || "").toLowerCase();
if (!normalized) return;
if (!normalized) return false;
if (
normalized.includes("felicahistorypage") ||
normalized.includes("open/felica")
) {
markStartupExplicitTarget();
navigateWhenReady(() => openFelicaPage(), url, retryCount);
} else if (normalized.includes("open/traininfo")) {
return true;
}
if (normalized.includes("open/traininfo")) {
markStartupExplicitTarget();
navigateWhenReady(() => {
rootNavigationRef.navigate("topMenu", { screen: "menu" } as any);
stackAwareNavigate("topMenu", { screen: "menu" });
setTimeout(() => {
SheetManager.show("JRSTraInfo");
}, 450);
}, url, retryCount);
} else if (normalized.includes("open/operation")) {
navigateWhenReady(() => {
rootNavigationRef.navigate("information" as any);
}, url, retryCount);
} else if (normalized.includes("open/settings")) {
navigateWhenReady(() => {
rootNavigationRef.navigate("topMenu", {
screen: "setting",
} as any);
}, url, retryCount);
} else if (normalized.includes("open/topmenu")) {
navigateWhenReady(() => {
rootNavigationRef.navigate("topMenu", {
screen: "menu",
} as any);
}, url, retryCount);
return true;
}
if (normalized.includes("open/operation")) {
markStartupExplicitTarget();
navigateWhenReady(() => {
stackAwareNavigate("information");
}, url, retryCount);
return true;
}
if (normalized.includes("open/settings")) {
markStartupExplicitTarget();
navigateWhenReady(() => {
stackAwareNavigate("topMenu", {
screen: "setting",
});
}, url, retryCount);
return true;
}
if (normalized.includes("open/topmenu")) {
markStartupExplicitTarget();
navigateWhenReady(() => {
stackAwareNavigate("topMenu", {
screen: "menu",
});
}, url, retryCount);
return true;
}
if (normalized.includes("positions/apps")) {
markStartupExplicitTarget();
navigateWhenReady(() => {
stackAwareNavigate("positions");
}, url, retryCount);
return true;
}
return false;
};
Linking.getInitialURL().then((url) => {
if (url) routeFromUrl(url);
});
Linking.getInitialURL()
.then((url) => {
if (url) routeFromUrl(url);
})
.finally(() => {
resolveStartupNavigationSource();
});
const sub = Linking.addEventListener("url", ({ url }) => {
routeFromUrl(url);
@@ -126,10 +235,10 @@ export default function App() {
StationListProvider,
FavoriteStationProvider,
TrainDelayDataProvider,
TrainMenuProvider, // CurrentTrainProvider より先に置くことで useTrainMenu が使える
CurrentTrainProvider,
AreaInfoProvider,
BusAndTrainDataProvider,
TrainMenuProvider,
SheetProvider,
]);
return (
@@ -137,13 +246,42 @@ export default function App() {
<DeviceOrientationChangeProvider>
<SafeAreaProvider>
<StatusbarDetect />
<GestureHandlerRootView style={{ flex: 1 }}>
<ProviderTree>
<AppContainer />
</ProviderTree>
</GestureHandlerRootView>
<DensityScaleWrapper>
<GestureHandlerRootView style={{ flex: 1 }}>
<ProviderTree>
<AppContainer />
</ProviderTree>
</GestureHandlerRootView>
</DensityScaleWrapper>
</SafeAreaProvider>
</DeviceOrientationChangeProvider>
</AppThemeProvider>
);
});
/**
* 低密度ディスプレイ(DeX等)で全体を transform scale で拡大。
* Dimensions.get をパッチ済みなので、コンポーネントは内部サイズに合わせてレイアウトする。
*/
function DensityScaleWrapper({ children }: { children: React.ReactNode }) {
const { width, height } = useRealWindowDimensions();
if (!IS_LOW_DENSITY) {
return <>{children}</>;
}
return (
<View style={{ width, height, overflow: "hidden" }}>
<View
style={{
width: width / DEX_SCALE,
height: height / DEX_SCALE,
transform: [{ scale: DEX_SCALE }],
transformOrigin: "top left",
}}
>
{children}
</View>
</View>
);
}
+575 -17
View File
@@ -1,7 +1,7 @@
import React from "react";
import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native";
import { NavigationContainer, DarkTheme, DefaultTheme, type EventArg } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Animated, Platform, ActivityIndicator, View, StyleSheet, useColorScheme } from "react-native";
import { Animated, Platform, ActivityIndicator, View, StyleSheet, StatusBar, useWindowDimensions, InteractionManager } from "react-native";
import { useNavigationState } from "@react-navigation/native";
import { useFonts } from "expo-font";
import { LinearGradient } from "expo-linear-gradient";
@@ -14,8 +14,21 @@ import { useTrainMenu } from "./stateBox/useTrainMenu";
import lineColorList from "./assets/originData/lineColorList";
import { stationIDPair } from "./lib/getStationList";
import "./components/ActionSheetComponents/sheets";
import { rootNavigationRef } from "./lib/rootNavigation";
import { lastObservedRootRouteRef, positionsLifecycleRef, rootNavigationRef } from "./lib/rootNavigation";
import {
startupExplicitTargetRef,
waitForStartupNavigationResolution,
} from "./lib/rootNavigation";
import { fixedColors } from "./lib/theme/colors";
import { useThemeColors } from "./lib/theme";
import * as Sentry from "@sentry/react-native";
import WebView from "react-native-webview";
import { AS } from "./storageControl";
import { STORAGE_KEYS } from "./constants/storage";
import {
recordAppLifecycleRootNavigation,
setAppLifecycleWebViewActive,
} from "./lib/observability/appLifecycleCrashSentinel";
type RootTabParamList = {
positions: undefined;
@@ -34,10 +47,219 @@ type TabProps = {
const Tab = createBottomTabNavigator<RootTabParamList>();
const HiddenStartupPreloadWebViews = ({
shouldPreloadPositions,
shouldPreloadInformation,
}: {
shouldPreloadPositions: boolean;
shouldPreloadInformation: boolean;
}) => {
const [positionsLoaded, setPositionsLoaded] = React.useState(false);
const [informationLoaded, setInformationLoaded] = React.useState(false);
React.useEffect(() => {
Sentry.setContext("startup_hidden_preload", {
shouldPreloadPositions,
shouldPreloadInformation,
positionsLoaded,
informationLoaded,
});
}, [informationLoaded, positionsLoaded, shouldPreloadInformation, shouldPreloadPositions]);
const hiddenStyle = React.useMemo(() => ({
position: "absolute" as const,
width: 1,
height: 1,
opacity: 0,
left: -1000,
top: -1000,
pointerEvents: "none" as const,
}), []);
const showPositions = shouldPreloadPositions && !positionsLoaded;
const showInformation = shouldPreloadInformation && !informationLoaded;
if (!showPositions && !showInformation) return null;
return (
<View pointerEvents="none" style={hiddenStyle}>
{showPositions ? (
<WebView
source={{ uri: "https://train.jr-shikoku.co.jp/" }}
originWhitelist={["https://train.jr-shikoku.co.jp"]}
javaScriptEnabled
setSupportMultipleWindows={false}
onLoadStart={() => {
setAppLifecycleWebViewActive("startup_hidden_positions", true);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "info",
message: "positions hidden preload loadStart",
});
}}
onLoadEnd={() => {
setAppLifecycleWebViewActive("startup_hidden_positions", false);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "info",
message: "positions hidden preload loadEnd",
});
setPositionsLoaded(true);
}}
onError={() => {
setAppLifecycleWebViewActive("startup_hidden_positions", false);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "error",
message: "positions hidden preload error",
});
}}
/>
) : null}
{showInformation ? (
<WebView
source={{ uri: "https://www.jr-shikoku.co.jp/info/" }}
originWhitelist={["https://www.jr-shikoku.co.jp"]}
javaScriptEnabled
setSupportMultipleWindows={false}
onLoadStart={() => {
setAppLifecycleWebViewActive("startup_hidden_operation", true);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "info",
message: "operation hidden preload loadStart",
});
}}
onLoadEnd={() => {
setAppLifecycleWebViewActive("startup_hidden_operation", false);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "info",
message: "operation hidden preload loadEnd",
});
setInformationLoaded(true);
}}
onError={() => {
setAppLifecycleWebViewActive("startup_hidden_operation", false);
Sentry.addBreadcrumb({
category: "startup.preload",
level: "error",
message: "operation hidden preload error",
});
}}
/>
) : null}
</View>
);
};
const PositionsRootGateContext = React.createContext({
shouldActivate: false,
isRootFocused: false,
});
const InformationRootGateContext = React.createContext({
shouldActivate: false,
isRootFocused: false,
});
const DeferredPositionsRoot = ({ shouldActivate, isRootFocused }: { shouldActivate: boolean; isRootFocused: boolean }) => {
const [hasActivated, setHasActivated] = React.useState(false);
React.useEffect(() => {
Sentry.setContext("positions_root_gate", {
focused: isRootFocused,
activated: hasActivated,
shouldActivate,
});
}, [hasActivated, isRootFocused, shouldActivate]);
React.useEffect(() => {
if (!shouldActivate || hasActivated) return;
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions root activation",
data: {
reason: "first_root_visit",
},
});
setHasActivated(true);
}, [hasActivated, shouldActivate]);
if (!hasActivated) {
return <View style={{ flex: 1 }} />;
}
return <Top />;
};
const PositionsTabScreen = React.memo(() => {
const { shouldActivate, isRootFocused } = React.useContext(PositionsRootGateContext);
return (
<DeferredPositionsRoot
shouldActivate={shouldActivate}
isRootFocused={isRootFocused}
/>
);
});
PositionsTabScreen.displayName = "PositionsTabScreen";
const DeferredInformationRoot = ({ shouldActivate, isRootFocused }: { shouldActivate: boolean; isRootFocused: boolean }) => {
const [hasActivated, setHasActivated] = React.useState(false);
React.useEffect(() => {
Sentry.setContext("operation_root_gate", {
focused: isRootFocused,
activated: hasActivated,
shouldActivate,
});
}, [hasActivated, isRootFocused, shouldActivate]);
React.useEffect(() => {
if (!shouldActivate || hasActivated) return;
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation root activation",
data: {
reason: isRootFocused ? "first_focus" : "background_prewarm",
},
});
setHasActivated(true);
}, [hasActivated, isRootFocused, shouldActivate]);
if (!hasActivated) {
return <View style={{ flex: 1 }} />;
}
return <TNDView />;
};
const InformationTabScreen = React.memo(() => {
const { shouldActivate, isRootFocused } = React.useContext(InformationRootGateContext);
return (
<DeferredInformationRoot
shouldActivate={shouldActivate}
isRootFocused={isRootFocused}
/>
);
});
InformationTabScreen.displayName = "InformationTabScreen";
export function AppContainer() {
const { areaInfo, areaIconBadgeText, isInfo } = useAreaInfo();
const { selectedLine } = useTrainMenu();
const { width, height } = useWindowDimensions();
const [isExtraWindowOpen, setIsExtraWindowOpen] = React.useState(false);
const [startupInitialRoute, setStartupInitialRoute] = React.useState<keyof RootTabParamList | null>(null);
const [hasVisitedPositions, setHasVisitedPositions] = React.useState(false);
const [hasVisitedInformation, setHasVisitedInformation] = React.useState(false);
const [currentRootRoute, setCurrentRootRoute] = React.useState<keyof RootTabParamList | null>(null);
const [navigationReady, setNavigationReady] = React.useState(false);
const startupPrewarmAttemptedRef = React.useRef(false);
const informationStartupPrewarmAttemptedRef = React.useRef(false);
// フェードアニメーション用 (0=通常, 1=追加ウィンドウ青)
const fadeAnim = React.useRef(new Animated.Value(0)).current;
@@ -60,17 +282,54 @@ export function AppContainer() {
const b = Math.round(parseInt(h.slice(4, 6), 16) * factor);
return `#${[r, g, b].map((v) => Math.min(255, v).toString(16).padStart(2, "0")).join("")}`;
};
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const { isDark } = useThemeColors();
const lastRootNavStateRef = React.useRef("");
const lineColorDark = lineColor ? darkenHex(lineColor, 0.78) : null;
React.useEffect(() => {
let cancelled = false;
void (async () => {
await waitForStartupNavigationResolution();
if (cancelled) return;
let nextInitialRoute: keyof RootTabParamList = "topMenu";
if (!startupExplicitTargetRef.current) {
try {
const startPage = await AS.getItem(STORAGE_KEYS.START_PAGE);
if (startPage === "true") {
nextInitialRoute = "positions";
}
} catch {
// Keep the default route when the setting is unset or unreadable.
}
}
if (cancelled) return;
setStartupInitialRoute(nextInitialRoute);
setHasVisitedPositions(nextInitialRoute === "positions");
Sentry.addBreadcrumb({
category: "nav.root",
level: "info",
message: "startup initial root route resolved",
data: {
startupExplicitTarget: startupExplicitTargetRef.current,
initialRoute: nextInitialRoute,
},
});
})();
return () => {
cancelled = true;
};
}, []);
const linking = {
prefixes: ["jrshikoku://"],
config: {
screens: {
positions: {
screens: {
Apps: "positions/apps",
},
screens: {},
},
topMenu: {
screens: {
@@ -101,17 +360,152 @@ export function AppContainer() {
tabBarLabel: label,
headerShown: false,
gestureEnabled: true,
unmountOnBlur: false,
tabBarIcon: initIcon(icon as any, iconFamily, tabBarBadge, isInfo),
},
});
const applyRootNavigationState = React.useCallback((state: any, source: "ready" | "change") => {
const activeRoute = state?.routes?.[state?.index ?? 0];
const hasExtra = (activeRoute?.state?.index ?? 0) > 0;
const nestedState = activeRoute?.state;
const nestedRoute = nestedState?.routes?.[nestedState.index ?? 0]?.name ?? null;
const signature = JSON.stringify({
rootIndex: state?.index ?? 0,
rootRoute: activeRoute?.name ?? null,
nestedIndex: nestedState?.index ?? 0,
nestedRoute,
hasExtra,
});
if (lastRootNavStateRef.current !== signature) {
lastRootNavStateRef.current = signature;
Sentry.addBreadcrumb({
category: "nav.root",
level: "info",
message: source === "ready" ? "root state initialized" : "root state change",
data: {
rootIndex: state?.index ?? 0,
rootRoute: activeRoute?.name ?? null,
nestedIndex: nestedState?.index ?? 0,
nestedRoute,
hasExtra,
source,
},
});
}
const rootRouteName = (activeRoute?.name ?? null) as keyof RootTabParamList | null;
if (rootRouteName) {
lastObservedRootRouteRef.current = rootRouteName;
}
recordAppLifecycleRootNavigation({
rootIndex: state?.index ?? 0,
rootRoute: rootRouteName,
nestedIndex: nestedState?.index ?? 0,
nestedRoute,
source,
});
Sentry.setTag("root_tab", rootRouteName ?? "unknown");
Sentry.setContext("root_navigation", {
rootIndex: state?.index ?? 0,
rootRoute: rootRouteName,
nestedIndex: nestedState?.index ?? 0,
nestedRoute,
hasExtra,
source,
operationOrientation: width > height ? "landscape" : "portrait",
hasVisitedPositions,
hasVisitedInformation,
});
setCurrentRootRoute(rootRouteName);
setIsExtraWindowOpen(hasExtra);
}, [hasVisitedInformation, hasVisitedPositions, width, height]);
const [fontLoaded, error] = useFonts({
"JR-Nishi": require("./assets/fonts/jr-nishi.otf"),
"JR-WEST-PLUS": require("./assets/fonts/JR-WEST-PLUS.otf"),
Zou: require("./assets/fonts/DelaGothicOne-Regular.ttf"),
"JNR-font": require("./assets/fonts/JNRfont_pict.ttf"),
"DiaPro": require("./assets/fonts/DiaPro-Regular.otf"),
});
if (!fontLoaded) {
React.useEffect(() => {
if (!navigationReady || !fontLoaded) return;
if (startupPrewarmAttemptedRef.current) return;
if (hasVisitedPositions) {
startupPrewarmAttemptedRef.current = true;
return;
}
if (currentRootRoute === null || currentRootRoute === "positions") return;
startupPrewarmAttemptedRef.current = true;
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions startup prewarm scheduled",
data: {
currentRootRoute,
},
});
let cancelled = false;
const timer = setTimeout(() => {
if (cancelled || hasVisitedPositions) return;
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions startup prewarm activated",
data: {
currentRootRoute,
},
});
setHasVisitedPositions(true);
}, 500);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [currentRootRoute, fontLoaded, hasVisitedPositions, navigationReady]);
React.useEffect(() => {
if (!navigationReady || !fontLoaded) return;
if (informationStartupPrewarmAttemptedRef.current) return;
if (hasVisitedInformation) {
informationStartupPrewarmAttemptedRef.current = true;
return;
}
if (currentRootRoute === null || currentRootRoute === "information") return;
informationStartupPrewarmAttemptedRef.current = true;
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation startup prewarm scheduled",
data: {
currentRootRoute,
},
});
let cancelled = false;
const timer = setTimeout(() => {
if (cancelled || hasVisitedInformation) return;
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation startup prewarm activated",
data: {
currentRootRoute,
},
});
setHasVisitedInformation(true);
}, 500);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [currentRootRoute, fontLoaded, hasVisitedInformation, navigationReady]);
if (!fontLoaded || !startupInitialRoute) {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<ActivityIndicator size="large" />
@@ -123,14 +517,123 @@ export function AppContainer() {
ref={rootNavigationRef}
linking={linking}
theme={isDark ? DarkTheme : DefaultTheme}
onReady={() => {
Sentry.addBreadcrumb({
category: "nav.root",
level: "info",
message: "navigation ready",
});
applyRootNavigationState(rootNavigationRef.getRootState?.() ?? rootNavigationRef.getState?.(), "ready");
setNavigationReady(true);
}}
onStateChange={(state) => {
const hasExtra = state?.routes?.some((r) => (r.state?.index ?? 0) > 0) ?? false;
setIsExtraWindowOpen(hasExtra);
applyRootNavigationState(state, "change");
}}
>
{/* @ts-expect-error - Tab.Navigator type definition issue */}
<PositionsRootGateContext.Provider
value={{
shouldActivate: hasVisitedPositions,
isRootFocused: currentRootRoute === "positions",
}}
>
<InformationRootGateContext.Provider
value={{
shouldActivate: hasVisitedInformation,
isRootFocused: currentRootRoute === "information",
}}
>
<Tab.Navigator
initialRouteName="topMenu"
id="rootTabs"
detachInactiveScreens={false}
initialRouteName={startupInitialRoute}
screenListeners={({ route }) => ({
tabPress: (event: EventArg<"tabPress", true>) => {
const rootState = rootNavigationRef.getState();
const activeRootRouteName =
(rootState?.routes?.[rootState.index ?? 0]?.name as keyof RootTabParamList | undefined) ??
currentRootRoute ??
null;
const leavingUnstablePositions =
route.name !== "positions" &&
positionsLifecycleRef.current.isUnstable;
const blockingUnstableExit =
route.name !== "positions" &&
positionsLifecycleRef.current.blockTabExit;
Sentry.addBreadcrumb({
category: "nav.tab",
level: "info",
message: blockingUnstableExit
? "root tabPress blocked while positions unstable"
: leavingUnstablePositions
? "root tabPress intercepted for unstable positions"
: "root tabPress",
data: {
route: route.name,
currentRootRoute,
activeRootRouteName,
unstablePositions: positionsLifecycleRef.current.isUnstable,
blockTabExit: positionsLifecycleRef.current.blockTabExit,
},
});
if (blockingUnstableExit) {
event.preventDefault();
positionsLifecycleRef.current.resetBeforeLeave?.();
requestAnimationFrame(() => {
setTimeout(() => {
rootNavigationRef.navigate(route.name as never);
}, 0);
});
return;
}
if (leavingUnstablePositions) {
event.preventDefault();
positionsLifecycleRef.current.resetBeforeLeave?.();
requestAnimationFrame(() => {
setTimeout(() => {
rootNavigationRef.navigate(route.name as never);
}, 0);
});
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions root exit reset and continued while session unstable",
data: {
route: route.name,
},
});
}
},
focus: () => {
Sentry.addBreadcrumb({
category: "nav.tab",
level: "info",
message: "root tab focus",
data: { route: route.name },
});
},
blur: () => {
Sentry.addBreadcrumb({
category: "nav.tab",
level: "info",
message: "root tab blur",
data: { route: route.name },
});
},
state: (event) => {
Sentry.addBreadcrumb({
category: "nav.tab",
level: "info",
message: "root tab state",
data: {
route: route.name,
stateType: event.type,
},
});
},
})}
screenOptions={({ route }) => {
const showGradient = route.name === "positions" && !!lineColor && !!lineColorDark;
const defaultBg = isDark ? "#1c1c1e" : "white";
@@ -138,8 +641,7 @@ export function AppContainer() {
const defaultInactive = isDark ? "#8e8e93" : "#8e8e93";
return {
lazy: false,
tabBarHideOnKeyboard: Platform.OS === "android",
animation: Platform.OS === "ios" ? "none" : "shift",
freezeOnBlur: false,
sceneContainerStyle: { backgroundColor: defaultBg },
tabBarActiveTintColor: (showGradient || isExtraWindowOpen) ? "white" : defaultActive,
tabBarInactiveTintColor: (showGradient || isExtraWindowOpen) ? "rgba(255,255,255,0.75)" : defaultInactive,
@@ -173,7 +675,32 @@ export function AppContainer() {
>
<Tab.Screen
{...getTabProps("positions", "走行位置", "bar-chart", "AntDesign")}
component={Top}
component={PositionsTabScreen}
listeners={{
tabPress: () => {
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions root tabPress activation",
});
setHasVisitedPositions(true);
},
focus: () => {
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions root focus activation",
});
setHasVisitedPositions(true);
},
blur: () => {
Sentry.addBreadcrumb({
category: "positions.root",
level: "info",
message: "positions root blur preserved",
});
},
}}
/>
<Tab.Screen
{...getTabProps("topMenu", "トップメニュー", "radio", "Ionicons")}
@@ -189,9 +716,40 @@ export function AppContainer() {
areaInfo ? areaIconBadgeText : undefined,
isInfo
)}
children={TNDView}
component={InformationTabScreen}
listeners={{
tabPress: () => {
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation root tabPress activation",
});
setHasVisitedInformation(true);
},
focus: () => {
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation root focus activation",
});
setHasVisitedInformation(true);
},
blur: () => {
Sentry.addBreadcrumb({
category: "operation.root",
level: "info",
message: "operation root blur preserved",
});
},
}}
/>
</Tab.Navigator>
</InformationRootGateContext.Provider>
</PositionsRootGateContext.Provider>
<HiddenStartupPreloadWebViews
shouldPreloadPositions={navigationReady && fontLoaded && currentRootRoute === "topMenu" && !hasVisitedPositions}
shouldPreloadInformation={navigationReady && fontLoaded && currentRootRoute === "topMenu" && !hasVisitedInformation}
/>
</NavigationContainer>
);
}
+484 -26
View File
@@ -1,25 +1,277 @@
import React, { CSSProperties } from "react";
import { Alert, BackHandler, View, ViewProps } from "react-native";
import React from "react";
import { Alert, ActivityIndicator, BackHandler, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import * as FileSystem from "expo-file-system/legacy";
import * as Sharing from "expo-sharing";
import { WebView } from "react-native-webview";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { BigButton } from "./components/atom/BigButton";
import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { useThemeColors } from "@/lib/theme";
import { AS } from "./storageControl";
import { STORAGE_KEYS } from "@/constants";
import { useTrainMenu } from "@/stateBox/useTrainMenu";
import {
DEFAULT_JR_DATA_SYSTEM_ENV,
normalizeJrDataSystemEnvironment,
rewriteJrDataSystemUrl,
} from "@/lib/jrDataSystemEnvironment";
const RECORDING_DOWNLOAD_BRIDGE_SCRIPT = `
(() => {
if (window.__JRS_RECORDING_DOWNLOAD_BRIDGE__) return true;
window.__JRS_RECORDING_DOWNLOAD_BRIDGE__ = true;
const blobUrls = new Map();
const post = (payload) => {
window.ReactNativeWebView?.postMessage(JSON.stringify(payload));
};
const isLikelyRecordingDownload = (url, anchor) => {
if (!url) return false;
const lowerUrl = String(url).toLowerCase();
const downloadName = anchor?.getAttribute?.('download') || '';
const lowerName = String(downloadName).toLowerCase();
return Boolean(downloadName)
|| lowerUrl.includes('recording')
|| lowerUrl.includes('recordings')
|| lowerUrl.includes('download')
|| lowerUrl.includes('export')
|| lowerUrl.includes('json')
|| lowerName.includes('recording')
|| lowerName.endsWith('.json');
};
const readUrlAsText = async (url) => {
if (blobUrls.has(url)) {
return blobUrls.get(url).text();
}
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.text();
};
const importFromUrl = async (url) => {
try {
const text = await readUrlAsText(url);
post({ type: 'importRecordingDownload', text, sourceUrl: url });
} catch (error) {
post({
type: 'importRecordingDownloadError',
message: error?.message || String(error),
sourceUrl: url,
});
}
};
const findAnchor = (target) => {
let node = target;
while (node && node !== document) {
if (node.tagName === 'A' && node.href) return node;
node = node.parentNode;
}
return null;
};
if (window.URL?.createObjectURL) {
const originalCreateObjectURL = window.URL.createObjectURL.bind(window.URL);
window.URL.createObjectURL = (object) => {
const url = originalCreateObjectURL(object);
if (object instanceof Blob) blobUrls.set(url, object);
return url;
};
}
document.addEventListener('click', (event) => {
const anchor = findAnchor(event.target);
if (!anchor || !isLikelyRecordingDownload(anchor.href, anchor)) return;
event.preventDefault();
event.stopPropagation();
importFromUrl(anchor.href);
}, true);
if (window.HTMLAnchorElement?.prototype?.click) {
const originalClick = window.HTMLAnchorElement.prototype.click;
window.HTMLAnchorElement.prototype.click = function patchedClick() {
if (isLikelyRecordingDownload(this.href, this)) {
importFromUrl(this.href);
return;
}
return originalClick.apply(this, arguments);
};
}
return true;
})();
true;
`;
const isLikelyRecordingDownloadUrl = (url: string) => {
const lowerUrl = url.toLowerCase();
return lowerUrl.includes('recording')
|| lowerUrl.includes('recordings')
|| lowerUrl.includes('download')
|| lowerUrl.includes('export')
|| lowerUrl.includes('json')
|| lowerUrl.endsWith('.json');
};
export default ({ route }) => {
if (!route.params) {
return null;
}
const { uri, useExitButton = true } = route.params;
const {
uri,
useExitButton = true,
importRecordingDownloads = false,
} = route.params;
const { goBack } = useNavigation();
const { fixed } = useThemeColors();
const { importRecordingsFromText } = useTrainMenu();
const webViewRef = React.useRef<WebView>(null);
const [canGoBack, setCanGoBack] = React.useState(false);
const nativeCanGoBackRef = React.useRef(false);
const historyStackRef = React.useRef<string[]>([]);
const [selectedEnvironment, setSelectedEnvironment] = React.useState(
DEFAULT_JR_DATA_SYSTEM_ENV,
);
const [resolvedUri, setResolvedUri] = React.useState("");
const [isEnvironmentReady, setIsEnvironmentReady] = React.useState(false);
const hasAlerted = React.useRef(false);
const [isLoading, setIsLoading] = React.useState(true);
const [hasError, setHasError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
// WebViewをforce remountするためのkey
const [webViewKey, setWebViewKey] = React.useState(0);
// RN-side watchdog: WebViewプロセスの死活確認用
const lastPongAt = React.useRef<number>(Date.now());
const isLoadingRef = React.useRef(true);
const hasErrorRef = React.useRef(false);
// コンテンツ消失検知用: bodyLen の最大値と連続白画面カウント
const maxBodyLenRef = React.useRef(0);
const blankCountRef = React.useRef(0);
const remount = React.useCallback(() => {
lastPongAt.current = Date.now(); // remount直後に誤検知しないようリセット
maxBodyLenRef.current = 0;
blankCountRef.current = 0;
setHasError(false);
hasErrorRef.current = false;
setIsLoading(true);
isLoadingRef.current = true;
setWebViewKey((k) => k + 1);
}, []);
React.useEffect(() => {
let isMounted = true;
const applyEnvironment = (value: unknown) => {
if (!isMounted) return;
const nextEnvironment = normalizeJrDataSystemEnvironment(value);
const rawUri = typeof uri === "string" ? uri : "";
historyStackRef.current = [];
nativeCanGoBackRef.current = false;
setCanGoBack(false);
setSelectedEnvironment(nextEnvironment);
setResolvedUri(
importRecordingDownloads
? rawUri
: rewriteJrDataSystemUrl(rawUri, nextEnvironment),
);
setIsEnvironmentReady(true);
};
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then(applyEnvironment)
.catch(() => applyEnvironment(DEFAULT_JR_DATA_SYSTEM_ENV));
return () => {
isMounted = false;
};
}, [uri, importRecordingDownloads]);
const handleReload = () => {
lastPongAt.current = Date.now();
setHasError(false);
hasErrorRef.current = false;
setIsLoading(true);
isLoadingRef.current = true;
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 {
const result = await importRecordingsFromText(content);
Alert.alert(
"録画データを読み込みました",
result.overwrittenCount > 0
? `${result.importedCount}件を読み込みました。${result.overwrittenCount}件は同じIDのため上書きしました。`
: `${result.importedCount}件を読み込みました。`,
);
} catch (error) {
Alert.alert(
"録画データを読み込めませんでした",
(error as Error).message || "ファイル内容を確認してください。",
);
}
},
[importRecordingsFromText],
);
const handleImportRecordingUrl = React.useCallback(
async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
await handleImportRecordingText(await response.text());
} catch (error) {
Alert.alert(
"録画データを取得できませんでした",
(error as Error).message || "通信状態を確認してください。",
);
}
},
[handleImportRecordingText],
);
useFocusEffect(
React.useCallback(() => {
const onHardwareBack = () => {
if (canGoBack) {
webViewRef.current?.goBack();
return true;
if (handleWebViewBack()) return true;
}
goBack();
return true;
@@ -27,31 +279,237 @@ export default ({ route }) => {
const subscription = BackHandler.addEventListener("hardwareBackPress", onHardwareBack);
return () => subscription.remove();
}, [canGoBack, goBack])
}, [canGoBack, goBack, handleWebViewBack])
);
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<WebView
source={{ uri }}
allowsBackForwardNavigationGestures
ref={webViewRef}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
goBack();
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
{isEnvironmentReady && (
<WebView
key={webViewKey}
source={{ uri: resolvedUri }}
contentMode="mobile"
allowsBackForwardNavigationGestures
setSupportMultipleWindows={false}
ref={webViewRef}
injectedJavaScriptBeforeContentLoaded={
importRecordingDownloads ? RECORDING_DOWNLOAD_BRIDGE_SCRIPT : `true;`
}
}}
onMessage={(event) => {
const { data } = event.nativeEvent;
const { type } = JSON.parse(data);
if (type === "back") return webViewRef.current?.goBack();
if (type === "windowClose") return goBack();
}}
/>
{useExitButton && <BigButton onPress={goBack} string="閉じる" />}
onLoadStart={() => {
isLoadingRef.current = true;
setHasError(false);
hasErrorRef.current = false;
maxBodyLenRef.current = 0;
blankCountRef.current = 0;
}}
onLoadEnd={() => {
setIsLoading(false);
isLoadingRef.current = false;
lastPongAt.current = Date.now();
}}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
setIsLoading(false);
isLoadingRef.current = false;
setHasError(true);
hasErrorRef.current = true;
setErrorMessage(nativeEvent.description || "ページを読み込めませんでした");
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
if (nativeEvent.statusCode >= 500) {
setIsLoading(false);
isLoadingRef.current = false;
setHasError(true);
hasErrorRef.current = true;
setErrorMessage(`サーバーエラー (${nativeEvent.statusCode})`);
}
}}
onRenderProcessGone={() => {
// クラッシュ・メモリ回収どちらも自動remount
remount();
}}
// iOS: コンテンツプロセスがメモリ圧迫で終了した場合
onContentProcessDidTerminate={() => remount()}
onShouldStartLoadWithRequest={(request) => {
if (request.isTopFrame === false) {
return true;
}
if (
importRecordingDownloads &&
isLikelyRecordingDownloadUrl(request.url) &&
request.url !== resolvedUri
) {
void handleImportRecordingUrl(request.url);
return false;
}
if (!importRecordingDownloads) {
const rewrittenUrl = rewriteJrDataSystemUrl(
request.url,
selectedEnvironment,
);
if (rewrittenUrl !== request.url) {
setResolvedUri(rewrittenUrl);
return false;
}
}
return true;
}}
onFileDownload={(event) => {
if (!importRecordingDownloads) return;
const downloadUrl = event.nativeEvent.downloadUrl;
if (downloadUrl) {
void handleImportRecordingUrl(downloadUrl);
}
}}
onNavigationStateChange={(navState) => {
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") {
webViewRef.current?.goBack();
if (!hasAlerted.current) {
hasAlerted.current = true;
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
}
}
}}
onMessage={(event) => {
const { data } = event.nativeEvent;
let parsed: any;
try {
parsed = JSON.parse(data);
} catch {
return;
}
const { type } = parsed;
if (type === "importRecordingDownload") {
void handleImportRecordingText(String(parsed.text ?? ""));
return;
}
if (type === "importRecordingDownloadError") {
Alert.alert(
"録画データを取得できませんでした",
parsed.message || "ダウンロード内容を読み取れませんでした。",
);
return;
}
if (type === "pong") {
lastPongAt.current = Date.now();
const bodyLen: number = parsed.bodyLen ?? 0;
// innerTextベース: 最大値を更新
if (bodyLen > maxBodyLenRef.current) maxBodyLenRef.current = bodyLen;
// 一度200文字超の表示テキストがあった後に20文字未満になったら白画面と判定
// SPA遷移中の一時的な空白を避けるため3回連続(15秒)で発火
if (maxBodyLenRef.current > 200 && bodyLen < 20) {
blankCountRef.current += 1;
if (blankCountRef.current >= 3) {
blankCountRef.current = 0;
maxBodyLenRef.current = 0;
remount();
}
} else {
blankCountRef.current = 0;
}
return;
}
if (type === "printHtml") {
(async () => {
try {
const path = FileSystem.cacheDirectory + "diagram.html";
await FileSystem.writeAsStringAsync(path, parsed.html, { encoding: FileSystem.EncodingType.UTF8 });
const ok = await Sharing.isAvailableAsync();
if (ok) {
await Sharing.shareAsync(path, { mimeType: "text/html", dialogTitle: "ダイヤグラムを共有" });
}
} catch (e) {
Alert.alert("エラー", "PDF出力の準備に失敗しました。");
}
})();
return;
}
if (type === "back") return handleWebViewBack();
if (type === "windowClose") return goBack();
}}
/>
)}
{isLoading && !hasError && (
<View style={wvStyles.loadingOverlay} pointerEvents="none">
<ActivityIndicator size="large" color="#fff" />
</View>
)}
{hasError && (
<View style={wvStyles.errorOverlay}>
<MaterialCommunityIcons name="wifi-off" size={48} color="#ccc" />
<Text style={wvStyles.errorText}>{errorMessage}</Text>
<TouchableOpacity style={wvStyles.reloadButton} onPress={handleReload}>
<MaterialCommunityIcons name="reload" size={18} color="#fff" />
<Text style={wvStyles.reloadButtonText}></Text>
</TouchableOpacity>
{useExitButton && (
<TouchableOpacity style={wvStyles.backButton} onPress={goBack}>
<Text style={wvStyles.backButtonText}></Text>
</TouchableOpacity>
)}
</View>
)}
{useExitButton && !hasError && <BigButton onPress={goBack} string="閉じる" />}
</View>
);
};
const wvStyles = StyleSheet.create({
loadingOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
backgroundColor: "rgba(0,0,0,0.25)",
},
errorOverlay: {
...StyleSheet.absoluteFillObject,
alignItems: "center",
justifyContent: "center",
backgroundColor: "#1a1a2e",
gap: 16,
paddingHorizontal: 32,
},
errorText: {
color: "#aaa",
fontSize: 14,
textAlign: "center",
},
reloadButton: {
flexDirection: "row",
alignItems: "center",
gap: 8,
backgroundColor: "#0099CC",
borderRadius: 10,
paddingHorizontal: 24,
paddingVertical: 12,
},
reloadButtonText: {
color: "#fff",
fontSize: 15,
fontWeight: "bold",
},
backButton: {
paddingHorizontal: 24,
paddingVertical: 10,
},
backButtonText: {
color: "#888",
fontSize: 14,
},
});
+100 -15
View File
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useWindowDimensions, Platform, useColorScheme } from "react-native";
import Constants from "expo-constants";
import { useResponsive } from "@/lib/responsive";
import { Dimensions, StatusBar } from "react-native";
@@ -17,31 +18,53 @@ import Setting from "@/components/Settings/settings";
import { useFavoriteStation } from "@/stateBox/useFavoriteStation";
import { optionData } from "@/lib/stackOption";
import { AllTrainDiagramView } from "@/components/AllTrainDiagramView";
import { useNavigation } from "@react-navigation/native";
import { useNavigation, useIsFocused } from "@react-navigation/native";
import { news } from "@/config/newsUpdate";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import GeneralWebView from "@/GeneralWebView";
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
import * as Sentry from "@sentry/react-native";
const Stack = createStackNavigator();
export function MenuPage() {
const { favoriteStation, setFavoriteStation } = useFavoriteStation();
const { height, width } = useWindowDimensions();
const { verticalScale } = useResponsive();
const tabBarHeight = useBottomTabBarHeight();
const navigation = useNavigation<any>();
const { addListener } = navigation;
const isFocused = useIsFocused();
const isDark = useColorScheme() === "dark";
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
useEffect(() => {
AS.getItem(STORAGE_KEYS.START_PAGE)
.then((res) => {
if (res == "true") navigation.navigate("positions");
})
.catch((e) => {
//6.0以降false
AS.setItem(STORAGE_KEYS.START_PAGE, "false");
});
useEffect(() => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu mounted",
});
return () => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu unmounted",
});
};
}, []);
useEffect(() => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: isFocused ? "top menu focused" : "top menu blurred",
data: {
width,
height,
},
});
}, [height, isFocused, width]);
useEffect(() => {
//ニュース表示
AS.getItem(STORAGE_KEYS.NEWS_STATUS)
.then((d) => {
@@ -53,10 +76,20 @@ export function MenuPage() {
if (isSetIcon == "true") SheetManager.show("TrainIconUpdate");
})
.catch((error) => logger.error("Error fetching icon setting:", error));
return undefined;
}, []);
const scrollRef = useRef(null);
const [mapMode, setMapMode] = useState(false);
useEffect(() => {
Sentry.setContext("top_menu_screen", {
focused: isFocused,
width,
height,
mapMode,
});
}, [height, isFocused, mapMode, width]);
const [mapHeight, setMapHeight] = useState(0);
const mapHeightRef = useRef(0);
const favoriteStationRef = useRef(favoriteStation);
@@ -65,8 +98,8 @@ export function MenuPage() {
height -
tabBarHeight +
(Platform.OS == "android" ? Constants.statusBarHeight : 0) -
100 -
((((width / 100) * 80) / 20) * 9 + 10 + 30);
verticalScale(100) -
((((width / 100) * 80) / 20) * 9 + verticalScale(10) + verticalScale(30));
setMapHeight(MapHeight);
mapHeightRef.current = MapHeight;
}, [height, tabBarHeight, width]);
@@ -82,9 +115,26 @@ export function MenuPage() {
setMapFullHeight(MapFullHeight);
}, [height, tabBarHeight, width]);
useEffect(() => {
const unsubscribe = addListener("tabPress", (e) => {
const unsubscribe = addListener("tabPress", (e: any) => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu tabPress handler",
data: {
focused: navigation.isFocused(),
stackIndex: stackNavRef.current?.getState()?.index ?? null,
mapMode,
},
});
if (navigation.isFocused() && stackNavRef.current) {
if (stackNavRef.current.getState()?.index > 0) {
e.preventDefault();
stackNavRef.current.goBack();
return;
}
}
scrollRef.current?.scrollTo({
y: mapHeightRef.current - 80,
y: mapHeightRef.current - verticalScale(80),
animated: true,
});
setMapMode(false);
@@ -104,10 +154,45 @@ export function MenuPage() {
return unsubscribe;
}, [navigation]);
const stackNavRef = useRef<any>(null);
return (
<Stack.Navigator
id={null}
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
detachInactiveScreens={false}
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
screenListeners={({ route, navigation: stackNav }) => {
stackNavRef.current = stackNav;
return {
focus: () => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu stack focus",
data: { route: route.name },
});
},
blur: () => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu stack blur",
data: { route: route.name },
});
},
state: () => {
Sentry.addBreadcrumb({
category: "topMenu.screen",
level: "info",
message: "top menu stack state",
data: {
route: route.name,
stackIndex: stackNav.getState()?.index ?? null,
},
});
},
};
}}
>
<Stack.Screen
name="menu"
+57 -1
View File
@@ -175,7 +175,63 @@ export const API_ENDPOINTS = {
};
```
## 📱 ビルド
## 外部からのアプリ起動(ディープリンク)
本アプリは複数の方法で外部から起動・画面遷移できます。
### カスタムURLスキーム
スキーム `jrshikoku://` を使って特定の画面を直接開くことができます。
| URL | 遷移先 |
|---|---|
| `jrshikoku://open/felica` | FeliCa履歴ページ |
| `jrshikoku://open/traininfo` | 遅延速報EX |
| `jrshikoku://open/operation` | 運行情報 |
| `jrshikoku://open/settings` | 設定 |
| `jrshikoku://open/topmenu` | トップメニュー |
| `jrshikoku://positions/apps` | 走行位置 |
URLの処理は `App.tsx``routeFromUrl` で実装されています。
### Androidウィジェット
ホーム画面に配置可能なウィジェットからアプリを起動できます。
| ウィジェット名 | 説明 |
|---|---|
| `JR_shikoku_train_info` | 遅延速報EX |
| `JR_shikoku_apps_shortcut` | クイックアクセス(各機能へのショートカットタイル) |
| `JR_shikoku_felica_balance` | ICカード残高表示 |
| `JR_shikoku_info` | 運行情報 |
| `JR_shikoku_train_strange` | 怪レい列車 |
ウィジェットのタップ時は `jrshikoku://` スキームでアプリ内画面へ遷移します。
実装: `components/AndroidWidget/widget-task-handler.tsx`
### iOSウィジェット(WidgetKit
WidgetKit拡張としてホーム画面ウィジェットを提供しています。Live Activities にも対応しています。
設定: `targets/widget/Info.plist`
### プッシュ通知
通知タップ時に対応する画面へ遷移します。
| アクション | 遷移先 |
|---|---|
| `delay-ex` | 遅延速報EX |
| `strange-train` | 走行位置(怪レい列車) |
| `information` | 運行情報 |
実装: `stateBox/useNotifications.tsx`
### NFCFeliCa
NFC-Fを利用した交通系ICカードの読み取りに対応しています(Android)。アプリ内からスキャンを開始する形式です。
実装: `modules/expo-felica-reader/`
## 📱 ビルド
```bash
# APKビルド(Android
+2 -4
View File
@@ -1,10 +1,8 @@
import React, { FC } from "react";
import { Platform, StatusBar, useColorScheme } from "react-native";
import { Platform, StatusBar } from "react-native";
const StatusbarDetect: FC = () => {
const isDark = useColorScheme() === "dark";
const barStyle = isDark ? "light-content" : "dark-content";
return <StatusBar barStyle={barStyle} translucent backgroundColor="transparent" />;
return <StatusBar barStyle="light-content" translucent backgroundColor="transparent" />;
};
export default StatusbarDetect;
+194 -10
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useNavigation } from "@react-navigation/native";
import { useIsFocused, useNavigation } from "@react-navigation/native";
import { useColorScheme } from "react-native";
import Apps from "./components/Apps";
import TrainBase from "./components/trainbaseview";
@@ -13,15 +13,20 @@ import { useCurrentTrain } from "./stateBox/useCurrentTrain";
import { useTrainMenu } from "./stateBox/useTrainMenu";
import { AS } from "./storageControl";
import { news } from "./config/newsUpdate";
import { Linking, Platform } from "react-native";
import { InteractionManager, Linking, Platform, View } from "react-native";
import GeneralWebView from "./GeneralWebView";
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
import { positionsLifecycleRef, positionsStackNavRef } from "./lib/rootNavigation";
import * as Sentry from "@sentry/react-native";
const Stack = createStackNavigator();
export const Top = () => {
const { webview } = useCurrentTrain();
const { navigate, addListener, isFocused } = useNavigation();
const navigation = useNavigation<any>();
const { navigate, addListener } = navigation;
const isTabFocused = useIsFocused();
const isDark = useColorScheme() === "dark";
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
const [hasActivatedStack, setHasActivatedStack] = useState(false);
//地図用
const { mapSwitch } = useTrainMenu();
@@ -30,36 +35,215 @@ export const Top = () => {
mapSwitchRef.current = mapSwitch;
}, [mapSwitch]);
const goToFavoriteList = () =>
const goToFavoriteList = (event?: any) => {
if (!hasActivatedStack) {
event?.preventDefault?.();
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions tabLongPress ignored before stack activation",
});
return;
}
navigate("positions", { screen: "favoriteList" });
};
useEffect(() => {
const unsubscribe = addListener("tabLongPress", goToFavoriteList);
return unsubscribe;
}, []);
const goToTrainMenu = useCallback(() => {
useEffect(() => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack mounted",
});
return () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack unmounted",
});
};
}, []);
useEffect(() => {
if (hasActivatedStack) return;
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack activation scheduled",
data: {
reason: isTabFocused ? "first_focus" : "background_prewarm",
},
});
let cancelled = false;
const activate = () => {
if (cancelled) return;
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack activation committed",
data: {
reason: isTabFocused ? "first_focus" : "background_prewarm",
},
});
setHasActivatedStack(true);
};
if (isTabFocused) {
const task = InteractionManager.runAfterInteractions(() => {
setTimeout(activate, 0);
});
return () => {
cancelled = true;
task.cancel?.();
};
}
const timer = setTimeout(activate, 300);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [hasActivatedStack, isTabFocused]);
useEffect(() => {
Sentry.setContext("positions_stack", {
focused: isTabFocused,
activated: hasActivatedStack,
mapSwitch,
});
}, [hasActivatedStack, isTabFocused, mapSwitch]);
const stackNavRef = positionsStackNavRef;
useEffect(() => {
positionsLifecycleRef.current = {
...positionsLifecycleRef.current,
deactivateStack: null,
};
return () => {
positionsLifecycleRef.current = {
...positionsLifecycleRef.current,
deactivateStack: null,
};
};
}, []);
const goToTrainMenu = useCallback((e: any) => {
const stackNav = stackNavRef.current;
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions tabPress handler",
data: {
platform: Platform.OS,
focused: isTabFocused,
mapSwitch: mapSwitchRef.current,
stackIndex: stackNav?.getState()?.index ?? null,
},
});
if (Platform.OS === "web") {
Linking.openURL("https://train.jr-shikoku.co.jp/");
setTimeout(() => navigate("topMenu", { screen: "menu" }), 100);
return;
}
if (!isFocused()) navigate("positions", { screen: "Apps" });
else if (mapSwitchRef.current == "true")
if (!hasActivatedStack || !stackNav) {
e?.preventDefault?.();
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions tabPress ignored before stack activation",
data: {
focused: isTabFocused,
activated: hasActivatedStack,
hasStackNav: !!stackNav,
},
});
return;
}
if (!isTabFocused) return;
if (stackNav && stackNav.getState()?.index > 0) {
e.preventDefault();
stackNav.goBack();
return;
}
if (mapSwitchRef.current == "true")
navigate("positions", { screen: "trainMenu" });
else webview.current?.injectJavaScript(`AccordionClassEvent()`);
return;
}, [isFocused, navigate, webview]);
}, [hasActivatedStack, isTabFocused, navigate, webview]);
useEffect(() => {
const unsubscribe = addListener("tabPress", goToTrainMenu);
return unsubscribe;
}, [addListener, goToTrainMenu]);
if (!hasActivatedStack) {
return <View style={{ flex: 1, backgroundColor: bgColor }} />;
}
return (
<Stack.Navigator
id={null}
screenOptions={{ contentStyle: { backgroundColor: bgColor } }}
detachInactiveScreens={false}
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
screenListeners={({ route, navigation: stackNav }) => {
stackNavRef.current = stackNav;
return {
focus: () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack focus",
data: { route: route.name },
});
},
blur: () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack blur",
data: { route: route.name },
});
},
transitionStart: () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack transitionStart",
data: {
route: route.name,
stackIndex: stackNav.getState()?.index ?? null,
},
});
},
transitionEnd: () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack transitionEnd",
data: {
route: route.name,
stackIndex: stackNav.getState()?.index ?? null,
},
});
},
state: () => {
Sentry.addBreadcrumb({
category: "positions.stack",
level: "info",
message: "positions stack state",
data: {
route: route.name,
stackIndex: stackNav.getState()?.index ?? null,
},
});
},
};
}}
>
<Stack.Screen
name="Apps"
+479 -18
View File
@@ -8,7 +8,7 @@
"android",
"web"
],
"version": "7.0.0",
"version": "7.1.0",
"userInterfaceStyle": "automatic",
"orientation": "default",
"icon": "./assets/icons/s8600.png",
@@ -24,7 +24,7 @@
"**/*"
],
"ios": {
"buildNumber": "60",
"buildNumber": "66",
"supportsTablet": true,
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
"appleTeamId": "54CRDT797G",
@@ -33,13 +33,18 @@
},
"infoPlist": {
"NFCReaderUsageDescription": "To read FeliCa card",
"NSPhotoLibraryUsageDescription": "運行情報画像を共有または保存するために写真ライブラリへのアクセスを使用します。",
"NSPhotoLibraryAddUsageDescription": "運行情報画像を写真ライブラリに保存するために使用します。",
"com.apple.developer.nfc.readersession.felica.systemcodes": [
"0003",
"FE00"
],
"ITSAppUsesNonExemptEncryption": false,
"NSSupportsLiveActivities": true,
"NSSupportsLiveActivitiesFrequentUpdates": true
"NSSupportsLiveActivitiesFrequentUpdates": true,
"UIBackgroundModes": [
"audio"
]
},
"entitlements": {
"com.apple.developer.nfc.readersession.formats": [
@@ -52,7 +57,7 @@
},
"android": {
"package": "jrshikokuinfo.xprocess.hrkn",
"versionCode": 30,
"versionCode": 32,
"intentFilters": [
{
"action": "VIEW",
@@ -105,12 +110,14 @@
},
"plugins": [
"./plugins/with-android-local-properties",
"./plugins/with-nfc-widget-guard",
"@bacons/apple-targets",
[
"expo-font",
{
"fonts": [
"./assets/fonts/jr-nishi.otf",
"./assets/fonts/JR-WEST-PLUS.otf",
"./assets/fonts/DelaGothicOne-Regular.ttf",
"./assets/fonts/JNRfont_pict.ttf",
"./assets/fonts/DiaPro-Regular.otf"
@@ -140,17 +147,7 @@
"minWidth": "70dp",
"minHeight": "50dp",
"description": "JR四国列車遅延速報EXのウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
"previewImage": "./assets/icon.png",
"updatePeriodMillis": 1800000,
"resizeMode": "horizontal|vertical"
},
{
"name": "JR_shikoku_train_strange",
"label": "怪レい列車",
"minWidth": "70dp",
"minHeight": "50dp",
"description": "JR四国怪レい列車BOTのウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
"previewImage": "./assets/icon.png",
"previewImage": "./assets/widgetResource/JR_shikoku_train_info.jpg",
"updatePeriodMillis": 1800000,
"resizeMode": "horizontal|vertical"
},
@@ -160,7 +157,7 @@
"minWidth": "70dp",
"minHeight": "50dp",
"description": "JR四国運行情報のウィジェットです。30分ごとに自動更新します。タッチすると強制更新します。",
"previewImage": "./assets/icon.png",
"previewImage": "./assets/widgetResource/JR_shikoku_info.jpg",
"updatePeriodMillis": 1800000,
"resizeMode": "horizontal|vertical"
},
@@ -170,7 +167,7 @@
"minWidth": "70dp",
"minHeight": "50dp",
"description": "JR四国非公式アプリの各種リンクを表示するウィジェットです。",
"previewImage": "./assets/icon.png",
"previewImage": "./assets/widgetResource/JR_shikoku_apps_shortcut.jpg",
"updatePeriodMillis": 1800000,
"resizeMode": "horizontal|vertical"
},
@@ -180,7 +177,7 @@
"minWidth": "70dp",
"minHeight": "50dp",
"description": "Felica対応ICカードの残高をホーム画面に表示するウィジェットです。タップでスキャン画面を開きます。",
"previewImage": "./assets/icon.png",
"previewImage": "./assets/widgetResource/JR_shikoku_felica_balance.jpg",
"updatePeriodMillis": 1800000,
"resizeMode": "horizontal|vertical"
}
@@ -509,6 +506,454 @@
"foregroundImage": "./assets/icons/ef210.png",
"backgroundColor": "#001413"
}
},
{
"name": "32ns",
"ios": "./assets/icons/s32ns.png",
"android": {
"foregroundImage": "./assets/icons/s32ns.png",
"backgroundColor": "#001413"
}
},
{
"name": "32s",
"ios": "./assets/icons/s32s.png",
"android": {
"foregroundImage": "./assets/icons/s32s.png",
"backgroundColor": "#001413"
}
},
{
"name": "32kpuy1",
"ios": "./assets/icons/s32kpuy1.png",
"android": {
"foregroundImage": "./assets/icons/s32kpuy1.png",
"backgroundColor": "#001413"
}
},
{
"name": "32kpuy2",
"ios": "./assets/icons/s32kpuy2.png",
"android": {
"foregroundImage": "./assets/icons/s32kpuy2.png",
"backgroundColor": "#001413"
}
},
{
"name": "32thtk",
"ios": "./assets/icons/s32thtk.png",
"android": {
"foregroundImage": "./assets/icons/s32thtk.png",
"backgroundColor": "#001413"
}
},
{
"name": "32oni1",
"ios": "./assets/icons/s32oni1.png",
"android": {
"foregroundImage": "./assets/icons/s32oni1.png",
"backgroundColor": "#001413"
}
},
{
"name": "32oni1k",
"ios": "./assets/icons/s32oni1k.png",
"android": {
"foregroundImage": "./assets/icons/s32oni1k.png",
"backgroundColor": "#001413"
}
},
{
"name": "32to4",
"ios": "./assets/icons/s32to4.png",
"android": {
"foregroundImage": "./assets/icons/s32to4.png",
"backgroundColor": "#001413"
}
},
{
"name": "32toai",
"ios": "./assets/icons/s32to_ai.png",
"android": {
"foregroundImage": "./assets/icons/s32to_ai.png",
"backgroundColor": "#001413"
}
},
{
"name": "54s",
"ios": "./assets/icons/s54s.png",
"android": {
"foregroundImage": "./assets/icons/s54s.png",
"backgroundColor": "#001413"
}
},
{
"name": "54to0ys",
"ios": "./assets/icons/s54to0ys.png",
"android": {
"foregroundImage": "./assets/icons/s54to0ys.png",
"backgroundColor": "#001413"
}
},
{
"name": "54nany1",
"ios": "./assets/icons/s54nany1.png",
"android": {
"foregroundImage": "./assets/icons/s54nany1.png",
"backgroundColor": "#001413"
}
},
{
"name": "54nany2",
"ios": "./assets/icons/s54nany2.png",
"android": {
"foregroundImage": "./assets/icons/s54nany2.png",
"backgroundColor": "#001413"
}
},
{
"name": "54smek1",
"ios": "./assets/icons/s32smek1.png",
"android": {
"foregroundImage": "./assets/icons/s32smek1.png",
"backgroundColor": "#001413"
}
},
{
"name": "40s",
"ios": "./assets/icons/s40.png",
"android": {
"foregroundImage": "./assets/icons/s40.png",
"backgroundColor": "#001413"
}
},
{
"name": "40w",
"ios": "./assets/icons/w40.png",
"android": {
"foregroundImage": "./assets/icons/w40.png",
"backgroundColor": "#001413"
}
},
{
"name": "185cm",
"ios": "./assets/icons/s185cm.png",
"android": {
"foregroundImage": "./assets/icons/s185cm.png",
"backgroundColor": "#001413"
}
},
{
"name": "185g",
"ios": "./assets/icons/s185g.png",
"android": {
"foregroundImage": "./assets/icons/s185g.png",
"backgroundColor": "#001413"
}
},
{
"name": "185tu_uzu",
"ios": "./assets/icons/s185tu_uzu.png",
"android": {
"foregroundImage": "./assets/icons/s185tu_uzu.png",
"backgroundColor": "#001413"
}
},
{
"name": "185ap1",
"ios": "./assets/icons/s185ap1.png",
"android": {
"foregroundImage": "./assets/icons/s185ap1.png",
"backgroundColor": "#001413"
}
},
{
"name": "185mm2",
"ios": "./assets/icons/s185mm2.png",
"android": {
"foregroundImage": "./assets/icons/s185mm2.png",
"backgroundColor": "#001413"
}
},
{
"name": "185ym2",
"ios": "./assets/icons/s185ym2.png",
"android": {
"foregroundImage": "./assets/icons/s185ym2.png",
"backgroundColor": "#001413"
}
},
{
"name": "1200",
"ios": "./assets/icons/s1200.png",
"android": {
"foregroundImage": "./assets/icons/s1200.png",
"backgroundColor": "#001413"
}
},
{
"name": "1201",
"ios": "./assets/icons/s1201.png",
"android": {
"foregroundImage": "./assets/icons/s1201.png",
"backgroundColor": "#001413"
}
},
{
"name": "1501",
"ios": "./assets/icons/s1501.png",
"android": {
"foregroundImage": "./assets/icons/s1501.png",
"backgroundColor": "#001413"
}
},
{
"name": "1550",
"ios": "./assets/icons/s1550.png",
"android": {
"foregroundImage": "./assets/icons/s1550.png",
"backgroundColor": "#001413"
}
},
{
"name": "1551",
"ios": "./assets/icons/s1551.png",
"android": {
"foregroundImage": "./assets/icons/s1551.png",
"backgroundColor": "#001413"
}
},
{
"name": "2000uwa",
"ios": "./assets/icons/s2000_uwa.png",
"android": {
"foregroundImage": "./assets/icons/s2000_uwa.png",
"backgroundColor": "#001413"
}
},
{
"name": "2000nl",
"ios": "./assets/icons/s2000nl.png",
"android": {
"foregroundImage": "./assets/icons/s2000nl.png",
"backgroundColor": "#001413"
}
},
{
"name": "2000-3",
"ios": "./assets/icons/s2000-3.png",
"android": {
"foregroundImage": "./assets/icons/s2000-3.png",
"backgroundColor": "#001413"
}
},
{
"name": "2000ganp1",
"ios": "./assets/icons/s2000ganp1.png",
"android": {
"foregroundImage": "./assets/icons/s2000ganp1.png",
"backgroundColor": "#001413"
}
},
{
"name": "2600apr",
"ios": "./assets/icons/s2600apr.png",
"android": {
"foregroundImage": "./assets/icons/s2600apr.png",
"backgroundColor": "#001413"
}
},
{
"name": "2600apb",
"ios": "./assets/icons/s2600apb.png",
"android": {
"foregroundImage": "./assets/icons/s2600apb.png",
"backgroundColor": "#001413"
}
},
{
"name": "2700asi",
"ios": "./assets/icons/s2700_asi.png",
"android": {
"foregroundImage": "./assets/icons/s2700_asi.png",
"backgroundColor": "#001413"
}
},
{
"name": "2700smn",
"ios": "./assets/icons/s2700_smn.png",
"android": {
"foregroundImage": "./assets/icons/s2700_smn.png",
"backgroundColor": "#001413"
}
},
{
"name": "2700uzu",
"ios": "./assets/icons/s2700_uzu.png",
"android": {
"foregroundImage": "./assets/icons/s2700_uzu.png",
"backgroundColor": "#001413"
}
},
{
"name": "2700apy1",
"ios": "./assets/icons/s2700apy1.png",
"android": {
"foregroundImage": "./assets/icons/s2700apy1.png",
"backgroundColor": "#001413"
}
},
{
"name": "2700apr1",
"ios": "./assets/icons/s2700apr1.png",
"android": {
"foregroundImage": "./assets/icons/s2700apr1.png",
"backgroundColor": "#001413"
}
},
{
"name": "3600",
"ios": "./assets/icons/s3600.png",
"android": {
"foregroundImage": "./assets/icons/s3600.png",
"backgroundColor": "#001413"
}
},
{
"name": "6000f",
"ios": "./assets/icons/s6000f.png",
"android": {
"foregroundImage": "./assets/icons/s6000f.png",
"backgroundColor": "#001413"
}
},
{
"name": "7001",
"ios": "./assets/icons/s7001.png",
"android": {
"foregroundImage": "./assets/icons/s7001.png",
"backgroundColor": "#001413"
}
},
{
"name": "8001nr",
"ios": "./assets/icons/s8001nr.png",
"android": {
"foregroundImage": "./assets/icons/s8001nr.png",
"backgroundColor": "#001413"
}
},
{
"name": "9000",
"ios": "./assets/icons/s9000.png",
"android": {
"foregroundImage": "./assets/icons/s9000.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640ht",
"ios": "./assets/icons/tosa9640ht.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640ht.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640mo1",
"ios": "./assets/icons/tosa9640mo1.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640mo1.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640mo2",
"ios": "./assets/icons/tosa9640mo2.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640mo2.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640tyg",
"ios": "./assets/icons/tosa9640tyg.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640tyg.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640tyb",
"ios": "./assets/icons/tosa9640tyb.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640tyb.png",
"backgroundColor": "#001413"
}
},
{
"name": "9640jbl",
"ios": "./assets/icons/tosa9640jbl.png",
"android": {
"foregroundImage": "./assets/icons/tosa9640jbl.png",
"backgroundColor": "#001413"
}
},
{
"name": "W741",
"ios": "./assets/icons/w741.png",
"android": {
"foregroundImage": "./assets/icons/w741.png",
"backgroundColor": "#001413"
}
},
{
"name": "EF210a",
"ios": "./assets/icons/ef210a.png",
"android": {
"foregroundImage": "./assets/icons/ef210a.png",
"backgroundColor": "#001413"
}
},
{
"name": "EF210n",
"ios": "./assets/icons/ef210n.png",
"android": {
"foregroundImage": "./assets/icons/ef210n.png",
"backgroundColor": "#001413"
}
},
{
"name": "EF210n1",
"ios": "./assets/icons/ef210n1.png",
"android": {
"foregroundImage": "./assets/icons/ef210n1.png",
"backgroundColor": "#001413"
}
},
{
"name": "EF210l3",
"ios": "./assets/icons/ef210l3.png",
"android": {
"foregroundImage": "./assets/icons/ef210l3.png",
"backgroundColor": "#001413"
}
},
{
"name": "T45000",
"ios": "./assets/icons/s150001to.png",
"android": {
"foregroundImage": "./assets/icons/s150001to.png",
"backgroundColor": "#001413"
}
},
{
"name": "Wk141",
"ios": "./assets/icons/w141jg.png",
"android": {
"foregroundImage": "./assets/icons/w141jg.png",
"backgroundColor": "#001413"
}
}
]
],
@@ -528,12 +973,28 @@
"expo-web-browser",
"expo-asset",
"expo-sharing",
[
"react-native-share",
{
"ios": [],
"android": [],
"enableBase64ShareAndroid": false
}
],
[
"react-native-maps",
{
"iosGoogleMapsApiKey": "AIzaSyAVGDTjBkR_0wkQiNkoo5WDLhqXCjrjk8Y",
"androidGoogleMapsApiKey": "AIzaSyAmFb-Yj033bXZWlSzNrfq_0jc1PgRrWcE"
}
],
[
"@sentry/react-native/expo",
{
"url": "https://sentry.io/",
"project": "jr-shikoku-unofficial-apps",
"organization": "xprocess-m5"
}
]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
西日本方向ロゴ拡張「JR-WEST-PLUS」
2026/07/11 by Mameh1nata
「回送」とか専用の書体の方がかっこよくね?と思い制作したフォントです
現時点で対応している種別は以下の通りです
回送 試運転 貸切 修学旅行 救援 配給 訓練 検測 競技 検定 教習
ver1.0 プライベート公開
ver1.01 修学旅行を追加
ver1.1 救援 配給 訓練の追加
ver1.2 競技 検定 教習の追加
-注意-
・このフォントは西日本方向幕ロゴの制作者とは何の関係もありません
・常識の範囲内で使うことをお勧めします(常識の範囲外でも自己責任でお願いします)
・文字サイズが多少違ったりするかもしれませんがご了承ください
・フォント初制作なのであまりにも致命的なミス以外は許してください
配布元: https://uu.getuploader.com/mameh1nata_dustbox/download/1
アプリ同梱版の調整:
・JR-Nishi と字面を揃えるため、各グリフの表示領域を 1040 units に正規化
・送り幅は 930 units とし、広げた字形の右側はアプリ側で小幅の DiaPro NBSP を付加して保護
・縦の表示領域を 836 units、ベースラインを -86〜750 に正規化
・フォント全体の ascender / descender を JR-Nishi と同じ 781 / -118 に調整
・文字コードと内部フォント名は元ファイルから維持
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

+93 -14
View File
@@ -1,45 +1,124 @@
export const series: { key: string; title: string; ids: string[] }[] = [
{ key: "32", title: "キハ32形", ids: ["32ns", "32s", "32kpuy1", "32kpuy2", "32tht", "32thtk", "32oni1", "32oni1k", "32toai", "32at"] },
{ key: "54", title: "キハ54形", ids: ["54s", "54st", "54nany1", "54nany2", "54smek1"] },
{ key: "40", title: "キハ40", ids: ["40s", "40w"] },
{ key: "185", title: "キハ185系", ids: ["185mrt", "185cm", "185g", "185tu", "185tu_uzu", "185iyor", "185iyoy", "185toai", "185mm1", "185mm2", "185ym1", "185ym2", "185ap1"] },
{ key: "1000", title: "1000形", ids: ["1000"] },
{ key: "1200", title: "1200形・1201形", ids: ["1200", "1201"] },
{ key: "1500", title: "1500形", ids: ["1501", "1550", "1551"] },
{ key: "2000", title: "2000系・N2000系", ids: ["2000asi", "2000uwa", "N2000", "2000nl", "2000-3", "2000ganp1", "2002a"] },
{ key: "2600", title: "2600系", ids: ["2600", "2600apr", "2600apb"] },
{ key: "2700", title: "2700系", ids: ["2700", "2700asi", "2700smn", "2700uzu", "2700apy", "2700apr", "2700apy1", "2700apr1"] },
{ key: "3600", title: "3600系", ids: ["3600"] },
{ key: "5000", title: "5000系", ids: ["5001", "5001k"] },
{ key: "6000", title: "6000系", ids: ["6000f"] },
{ key: "7000", title: "7000系", ids: ["7000"] },
{ key: "7200", title: "7200系", ids: ["7200"] },
{ key: "8000", title: "8000系", ids: ["8000no", "8000nr", "8001nr", "8000ap", "8000nn"] },
{ key: "8600", title: "8600系", ids: ["8600"] },
{ key: "9000", title: "9000系", ids: ["9000"] },
{ key: "9640", title: "9640形", ids: ["9640", "9640ht", "9640mo1", "9640mo2", "9640tyg", "9640tyb", "9640jgr", "9640jbl"] },
{ key: "other", title: "JR西日本・他", ids: ["285", "213w", "W741", "Wk141", "EF65", "EF210a", "EF210n", "EF210n1", "EF210l3", "T45000"] },
];
export default () =>{
return [
{ "id": "32", "name": "キハ32形", "icon": require("./32.png") },
{ "id": "32kpuuy", "name": "キハ32形かっぱうようよ号", "icon": require("./32kpuuy.png") },
//{ "id": "32", "name": "キハ32形", "icon": require("./32.png") },
{ "id": "32ns", "name": "キハ32形(標準色)", "icon": require("./s32ns.png") },
{ "id": "32s", "name": "キハ32形(新塗装)", "icon": require("./s32s.png") },
//{ "id": "32kpuuy", "name": "キハ32形かっぱうようよ号", "icon": require("./32kpuuy.png") },
{ "id": "32kpuy1", "name": "キハ32形かっぱうようよ号①", "icon": require("./s32kpuy1.png") },
{ "id": "32kpuy2", "name": "キハ32形かっぱうようよ号②", "icon": require("./s32kpuy2.png") },
{ "id": "32tht", "name": "キハ32形新幹線ホビートレイン", "icon": require("./32tht.png") },
{ "id": "32thtk", "name": "キクハ32形新幹線ホビートレイン", "icon": require("./s32thtk.png") },
{ "id": "32oni1", "name": "キハ32形特別塗装①", "icon": require("./s32oni1.png") },
{ "id": "32oni1k", "name": "キハ32形特別塗装①(連結)", "icon": require("./s32oni1k.png") },
//{ "id": "32to4", "name": "キハ32形(土讃色)", "icon": require("./s32to4.png") },
{ "id": "32toai", "name": "キクハ32形藍よしのがわトロッコ", "icon": require("./s32to_ai.png") },
{ "id": "32at", "name": "キクハ32形アンパンマントロッコ", "icon": require("./32at.png") },
{ "id": "54", "name": "キハ54形", "icon": require("./54.png") },
//{ "id": "54", "name": "キハ54形", "icon": require("./54.png") },
{ "id": "54s", "name": "キハ54形(四国色)", "icon": require("./s54s.png") },
{ "id": "54st", "name": "キハ54形しまんトロッコ", "icon": require("./54st.png") },
{ "id": "40", "name": "キハ40", "icon": require("./40.png") },
//{ "id": "54to0ys", "name": "キハ54形(ゆとりすと)", "icon": require("./s54to0ys.png") },
{ "id": "54nany1", "name": "キハ54形なんよ号①", "icon": require("./s54nany1.png") },
{ "id": "54nany2", "name": "キハ54形なんよ号②", "icon": require("./s54nany2.png") },
{ "id": "54smek1", "name": "キハ54形しまんトロッコ(連結)", "icon": require("./s32smek1.png") },
//{ "id": "40", "name": "キハ40", "icon": require("./40.png") },
{ "id": "40s", "name": "キハ40(四国色)", "icon": require("./s40.png") },
{ "id": "40w", "name": "キハ40(JR西日本色)", "icon": require("./w40.png") },
{ "id": "185mrt", "name": "キハ185系四国色", "icon": require("./s185_mrt.png") },
{ "id": "185cm", "name": "キハ185系普通列車色", "icon": require("./s185cm.png") },
{ "id": "185g", "name": "キハ185系(緑帯)", "icon": require("./s185g.png") },
{ "id": "185tu", "name": "キハ185系剣山色", "icon": require("./s185tu.png") },
{ "id": "185tu_uzu", "name": "キハ185系剣山色(うずしお)", "icon": require("./s185tu_uzu.png") },
{ "id": "185iyor", "name": "キハ185系伊予灘ものがたり(赤)", "icon": require("./s185iyor.png") },
{ "id": "185iyoy", "name": "キハ185系伊予灘ものがたり(黄)", "icon": require("./s185iyoy.png") },
{ "id": "185toai", "name": "キハ185系藍よしのがわトロッコ", "icon": require("./s185to_ai.png") },
{ "id": "185mm1", "name": "キハ185系四国まんなか千年ものがたり(緑)", "icon": require("./s185mm1.png") },
{ "id": "185mm2", "name": "キハ185系四国まんなか千年ものがたり(赤)", "icon": require("./s185mm2.png") },
{ "id": "185ym1", "name": "キハ185系時代の夜明けのものがたり(茶)", "icon": require("./s185ym1.png") },
{ "id": "185ym2", "name": "キハ185系時代の夜明けのものがたり(緑)", "icon": require("./s185ym2.png") },
{ "id": "185ap1", "name": "キハ185系アンパンマン", "icon": require("./s185ap1.png") },
{ "id": "1000", "name": "1000形", "icon": require("./s1000.png") },
{ "id": "1200n", "name": "1200形", "icon": require("./s1200n.png") },
{ "id": "1500", "name": "1500形", "icon": require("./s1500.png") },
{ "id": "5001", "name": "5000系(二階建て)", "icon": require("./s5001.png") },
{ "id": "5001k", "name": "5000系(平屋側)", "icon": require("./s5001k.png") },
{ "id": "6000p", "name": "6000系", "icon": require("./s6000p.png") },
{ "id": "7000", "name": "7000系", "icon": require("./s7000.png") },
{ "id": "7200", "name": "7200系", "icon": require("./s7200.png") },
//{ "id": "1200n", "name": "1200形", "icon": require("./s1200n.png") },
{ "id": "1200", "name": "1200形(旧塗装)", "icon": require("./s1200.png") },
{ "id": "1201", "name": "1201形", "icon": require("./s1201.png") },
//{ "id": "1500", "name": "1500形", "icon": require("./s1500.png") },
{ "id": "1501", "name": "1500形 1501", "icon": require("./s1501.png") },
{ "id": "1550", "name": "1500形 1550", "icon": require("./s1550.png") },
{ "id": "1551", "name": "1500形 1551", "icon": require("./s1551.png") },
{ "id": "2000asi", "name": "2000系", "icon": require("./s2000_asi.png") },
{ "id": "2000uwa", "name": "2000系(宇和海色)", "icon": require("./s2000_uwa.png") },
{ "id": "N2000", "name": "N2000系", "icon": require("./s2000n.png") },
{ "id": "2000nl", "name": "N2000系(南風色)", "icon": require("./s2000nl.png") },
{ "id": "2000-3", "name": "N2000系(試作)", "icon": require("./s2000-3.png") },
{ "id": "2000ganp1", "name": "2000系(復活塗装)", "icon": require("./s2000ganp1.png") },
{ "id": "2002a", "name": "2000系アンパンマン", "icon": require("./s2002a.png") },
{ "id": "2600", "name": "2600系" , "icon": require("./s2600.png")},
{ "id": "2600apr", "name": "2600系アンパンマン(赤)", "icon": require("./s2600apr.png") },
{ "id": "2600apb", "name": "2600系アンパンマン(青)", "icon": require("./s2600apb.png") },
{ "id": "2700", "name": "2700系", "icon": require("./s2700.png") },
{ "id": "2700asi", "name": "2700系(あしずり色)", "icon": require("./s2700_asi.png") },
{ "id": "2700smn", "name": "2700系(しまんと色)", "icon": require("./s2700_smn.png") },
{ "id": "2700uzu", "name": "2700系(うずしお色)", "icon": require("./s2700_uzu.png") },
{ "id": "2700apy", "name": "2700系アンパンマン(黄)", "icon": require("./s2700apy.png") },
{ "id": "2700apr", "name": "2700系アンパンマン(赤)", "icon": require("./s2700apr.png") },
{ "id": "2700apy1", "name": "2700系アンパンマン(黄)①", "icon": require("./s2700apy1.png") },
{ "id": "2700apr1", "name": "2700系アンパンマン(赤)①", "icon": require("./s2700apr1.png") },
{ "id": "3600", "name": "3600系", "icon": require("./s3600.png") },
{ "id": "5001", "name": "5000系(二階建て)", "icon": require("./s5001.png") },
{ "id": "5001k", "name": "5000系(平屋側)", "icon": require("./s5001k.png") },
//{ "id": "6000p", "name": "6000系", "icon": require("./s6000p.png") },
{ "id": "6000f", "name": "6000系(Fライナー)", "icon": require("./s6000f.png") },
{ "id": "7000", "name": "7000系", "icon": require("./s7000.png") },
//{ "id": "7001", "name": "7000系 7001", "icon": require("./s7001.png") },
{ "id": "7200", "name": "7200系", "icon": require("./s7200.png") },
{ "id": "8000no", "name": "8000系(オレンジ)", "icon": require("./s8000no.png") },
{ "id": "8000nr", "name": "8000系(赤)", "icon": require("./s8000nr.png") },
{ "id": "8001nr", "name": "8000系(赤) 8001", "icon": require("./s8001nr.png") },
{ "id": "8000ap", "name": "8000系アンパンマン", "icon": require("./s8000ap.png") },
{ "id": "8000nn", "name": "8000系リニューアル改", "icon": require("./s8000nn.png") },
{ "id": "8600", "name": "8600系", "icon": require("./s8600.png") },
{ "id": "9000", "name": "9000系", "icon": require("./s9000.png") },
{ "id": "9640", "name": "9640形(白)", "icon": require("./tosa9640.png") },
{ "id": "9640ht", "name": "9640形(赤帯)", "icon": require("./tosa9640ht.png") },
{ "id": "9640mo1", "name": "9640形もみじ号①", "icon": require("./tosa9640mo1.png") },
{ "id": "9640mo2", "name": "9640形もみじ号②", "icon": require("./tosa9640mo2.png") },
{ "id": "9640tyg", "name": "9640形(緑)", "icon": require("./tosa9640tyg.png") },
{ "id": "9640tyb", "name": "9640形(青)", "icon": require("./tosa9640tyb.png") },
{ "id": "9640jgr", "name": "9640形オープンデッキ(緑)", "icon": require("./tosa9640jgr.png") },
{ "id": "9640jbl", "name": "9640形オープンデッキ(青)", "icon": require("./tosa9640jbl.png") },
{ "id": "285", "name": "285系サンライズ瀬戸", "icon": require("./w285.png") },
{ "id": "213w", "name": "ラ・マル・ド・ボァ", "icon": require("./w213w.png") },
{ "id": "W741", "name": "DEC741系", "icon": require("./w741.png") },
{ "id": "Wk141", "name": "キヤ141系", "icon": require("./w141jg.png") },
{ "id": "EF65", "name": "EF65", "icon": require("./ef65.png") },
{ "id": "EF210", "name": "EF210", "icon": require("./ef210.png") },
//{ "id": "EF210", "name": "EF210", "icon": require("./ef210.png") },
{ "id": "EF210a", "name": "EF210(300番代)", "icon": require("./ef210a.png") },
{ "id": "EF210n", "name": "EF210(新塗装)", "icon": require("./ef210n.png") },
{ "id": "EF210n1", "name": "EF210(新塗装①)", "icon": require("./ef210n1.png") },
{ "id": "EF210l3", "name": "EF210(L3編成)", "icon": require("./ef210l3.png") },
{ "id": "T45000", "name": "トラ45000", "icon": require("./s150001to.png") },
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

+14
View File
@@ -18,4 +18,18 @@ export default [
lng: 133.816444,
isSpot: true,
},
{
Station_JP: ".小歩危展望台",
Station_EN: "Koboke Observatory",
MyStation: "0",
StationNumber: null,
DispNum: "3",
StationTimeTable: "",
StationMap: "https://maps.app.goo.gl/WBMN5R2tk2tusavk7",
JrHpUrl: "https://miyoshi-tourism.jp/spot/5438/",
jslodApi: "spot",
lat: 33.9372609,
lng: 133.753258,
isSpot: true,
},
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -1,21 +1,35 @@
import React, { useRef } from "react";
import React, { useRef, useState } from "react";
import { Platform } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { EachTrainInfoCore } from "./EachTrainInfoCore";
import { useSheetMaxHeight } from "./useSheetMaxHeight";
export const EachTrainInfo = ({ payload }) => {
if (!payload) return <></>;
const actionSheetRef = useRef(null);
const maxHeight = useSheetMaxHeight();
const [sheetOpened, setSheetOpened] = useState(false);
const handleOpen = () => {
setSheetOpened(true);
};
const handleClose = () => {
setSheetOpened(false);
};
if (!payload) return <></>;
return (
<ActionSheet
gestureEnabled={true}
CustomHeaderComponent={<></>}
ref={actionSheetRef}
drawUnderStatusBar={false}
isModal={Platform.OS == "ios"}
isModal={Platform.OS === "ios" && !Platform.isPad}
containerStyle={{ maxHeight }}
onOpen={handleOpen}
onClose={handleClose}
//useBottomSafeAreaPadding={Platform.OS == "android"}
>
<EachTrainInfoCore {...{ actionSheetRef, ...payload }} />
<EachTrainInfoCore {...{ actionSheetRef, sheetOpened, ...payload }} />
</ActionSheet>
);
};
@@ -2,6 +2,7 @@ import React, { FC } from "react";
import { View, Text, TouchableWithoutFeedback, Alert } from "react-native";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
export const DataConnectedButton: FC<{
i: string;
@@ -10,6 +11,7 @@ export const DataConnectedButton: FC<{
const [station, se, time] = i.split(",");
const { keyList } = useAllTrainDiagram();
const { colors } = useThemeColors();
const { fontScale, moderateScale } = useResponsive();
// 列番が有効かどうかをチェックする関数
const isValidTrainNumber = (trainNum: string): boolean => {
return keyList.includes(trainNum);
@@ -44,19 +46,19 @@ export const DataConnectedButton: FC<{
>
<View
style={{
width: 35,
width: moderateScale(35),
position: "relative",
marginHorizontal: 15,
flexDirection: "row",
height: "10%",
}}
/>
<Text style={{ fontSize:16, fontFamily: "DiaPro", color: colors.text }}>
<Text style={{ fontSize: fontScale(16), fontFamily: "DiaPro", color: colors.text }}>
{se === "増" ? "⬐" : "↳"}
</Text>
<Text style={{ fontSize: 20, color: colors.textLink }}>{time}</Text>
<Text style={{ fontSize: fontScale(20), color: colors.textLink }}>{time}</Text>
<View style={{ flex: 1 }} />
<Text style={{ fontSize: 18, width: 50, color: colors.text }}>
<Text style={{ fontSize: fontScale(18), width: moderateScale(50), color: colors.text }}>
{se === "増" ? "増結" : "解結"}
</Text>
</View>
@@ -12,6 +12,7 @@ import {
} from "@/utils/seUtils";
import type { SeTypes } from "@/types";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
type seTypes =
| "発編"
@@ -65,6 +66,7 @@ export const EachStopList: FC<props> = ({
isNotService = false,
}) => {
const { colors: themeColors, isDark } = useThemeColors();
const { fontScale, moderateScale } = useResponsive();
const [station, se, time, platformNum] = i.split(",") as [
string,
seTypes,
@@ -120,6 +122,16 @@ export const EachStopList: FC<props> = ({
const StationNumbers = Stations.filter((d) => d.StationNumber != null).map(
(d) => d.StationNumber as string
);
const trimmedPlatformNum = platformNum?.trim();
const hasPlatformNum = !!trimmedPlatformNum;
const isPlatformNumNumeric = hasPlatformNum && /^\d+$/.test(trimmedPlatformNum);
const platformDisplay = hasPlatformNum
? isPlatformNumNumeric
? parseInt(trimmedPlatformNum, 10) === 0
? "⓪"
: String.fromCharCode(0x2460 + parseInt(trimmedPlatformNum, 10) - 1)
: `(${trimmedPlatformNum})`
: "";
// SE文字列を表示用に変換
const [seString, seType] = parseSeString(se);
@@ -197,7 +209,7 @@ export const EachStopList: FC<props> = ({
>
<View
style={{
width: 35,
width: moderateScale(35),
position: "relative",
marginHorizontal: 15,
flexDirection: "row",
@@ -222,7 +234,7 @@ export const EachStopList: FC<props> = ({
<View style={{ position: "relative", height: 25 }}>
<Text
style={{
fontSize: 20,
fontSize: fontScale(20),
position: "absolute",
textAlignVertical: "center",
left: -15,
@@ -247,16 +259,24 @@ export const EachStopList: FC<props> = ({
<View style={{ flex: 1 }} />
<Text
style={{
fontSize: 20,
fontSize: fontScale(20),
color: colors.text,
fontStyle: isCommunity ? "italic" : "normal",
textAlignVertical: "center",
}}
>
{station}{platformNum &&
(parseInt(platformNum) === 0
? "⓪"
: String.fromCharCode(0x2460 + parseInt(platformNum) - 1))}
{station}
{hasPlatformNum && (
<Text
style={{
fontSize: fontScale(isPlatformNumNumeric ? 20 : 14),
color: colors.text,
fontStyle: isCommunity ? "italic" : "normal",
}}
>
{platformDisplay}
</Text>
)}
</Text>
<View style={{ flex: 1 }} />
</View>
@@ -308,20 +328,21 @@ const TimeText: FC<{
normalTextColor,
}) => {
const [seString, seType] = parseSeString(se);
const { fontScale, moderateScale } = useResponsive();
return (
<View style={{ flexDirection: "row", alignItems: "center" }}>
<View style={{ flexDirection: "row", alignItems: "center", opacity: isBefore ? 0.5 : 1 }}>
{!!currentTrainData?.delay &&
currentTrainData?.delay != "入線" &&
currentTrainData?.delay != 0 && (
<Text
style={{
fontSize: isBefore ? 14 : 15,
fontSize: fontScale(isBefore ? 14 : 15),
marginVertical: isDouble ? -2 : 0,
color: normalTextColor,
width: 60,
width: moderateScale(60),
position: "absolute",
right: isBefore ? 125 : 120,
right: isBefore ? moderateScale(125) : moderateScale(120),
textAlign: "right",
textDecorationLine: "line-through",
fontStyle: seType == "community" ? "italic" : "normal",
@@ -341,8 +362,8 @@ const TimeText: FC<{
/>
<Text
style={{
fontSize: isBefore ? 14 : 18,
width: 50,
fontSize: fontScale(isBefore ? 14 : 18),
width: moderateScale(50),
color: textColor,
marginVertical: isDouble ? -2 : 0,
fontStyle: seType == "community" ? "italic" : "normal",
@@ -356,6 +377,7 @@ const TimeText: FC<{
const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
const { stn, se } = props;
const { fixed } = useThemeColors();
const { fontScale } = useResponsive();
const lineColor = lineColorList[stn.charAt(0)];
const hasThrew =
se == "通過" ||
@@ -375,7 +397,7 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
style={{
color: fixed.textOnPrimary,
textAlign: "center",
fontSize: 10,
fontSize: fontScale(10),
fontWeight: "bold",
}}
>
@@ -400,6 +422,7 @@ type StationTimeBoxType = {
const StationTimeBox: FC<StationTimeBoxType> = (props) => {
const { delay, textColor, seType, se, time, isDouble, isBefore } = props;
const { fontScale, moderateScale } = useResponsive();
const dates = dayjs()
.set("hour", parseInt(time.split(":")[0]))
.set("minute", parseInt(time.split(":")[1]))
@@ -408,10 +431,10 @@ const StationTimeBox: FC<StationTimeBoxType> = (props) => {
return (
<Text
style={{
fontSize: isBefore ? 14 : 20,
fontSize: fontScale(isBefore ? 14 : 20),
marginVertical: isDouble ? -2 : 0,
color: textColor,
width: 60,
width: moderateScale(60),
fontStyle: seType == "community" ? "italic" : "normal",
}}
>
@@ -0,0 +1,144 @@
import React, { FC, useCallback, useEffect } from "react";
import { Platform, TouchableOpacity, Text, StyleSheet } from "react-native";
import { useTrainFollowActivity } from "@/lib/useTrainFollowActivity";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { trainDataType } from "@/lib/trainPositionTextArray";
type Props = {
currentTrainData: trainDataType;
currentPosition: string[] | undefined;
};
export const LiveActivityButton: FC<Props> = ({
currentTrainData,
currentPosition,
}) => {
const liveActivity = useTrainFollowActivity();
const { allTrainDiagram } = useAllTrainDiagram();
const onLine = !!currentPosition?.toString().length;
const handleLiveActivity = useCallback(async () => {
if (liveActivity.status === "active") {
await liveActivity.end();
return;
}
if (!onLine) return;
const diagramStr: string =
(allTrainDiagram as Record<string, string>)[currentTrainData?.num] ?? "";
const stations = diagramStr
.split("#")
.filter(Boolean)
.map((s) => s.split(",")[0]);
const destination =
stations.length > 0 ? stations[stations.length - 1] : "";
const curSt = currentPosition?.[0] ?? currentTrainData?.Pos ?? "";
const curIdx = stations.indexOf(curSt);
const nextSt =
curIdx >= 0 && curIdx + 1 < stations.length
? stations[curIdx + 1]
: currentPosition?.[1] ?? "";
await liveActivity.start({
trainNumber: currentTrainData?.num ?? "",
lineName: currentTrainData?.Line ?? "",
destination,
currentStation: curSt,
nextStation: nextSt,
delayMinutes:
typeof currentTrainData?.delay === "number"
? currentTrainData.delay
: 0,
scheduledArrival: "",
});
}, [
liveActivity,
onLine,
allTrainDiagram,
currentTrainData,
currentPosition,
]);
// 列車位置が変わったら Live Activity を自動更新
useEffect(() => {
if (liveActivity.status !== "active") return;
if (!currentTrainData?.Pos) return;
const diagramStr: string =
(allTrainDiagram as Record<string, string>)[currentTrainData?.num] ?? "";
const stations = diagramStr
.split("#")
.filter(Boolean)
.map((s) => s.split(",")[0]);
const curSt = currentPosition?.[0] ?? currentTrainData.Pos;
const curIdx = stations.indexOf(curSt);
const nextSt =
curIdx >= 0 && curIdx + 1 < stations.length
? stations[curIdx + 1]
: currentPosition?.[1] ?? "";
liveActivity.update({
currentStation: curSt,
nextStation: nextSt,
delayMinutes:
typeof currentTrainData.delay === "number"
? currentTrainData.delay
: 0,
scheduledArrival: "",
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
currentTrainData?.Pos,
currentTrainData?.delay,
currentPosition?.toString(),
]);
if (Platform.OS !== "ios" || !liveActivity.available || !onLine) {
return null;
}
return (
<TouchableOpacity
onPress={handleLiveActivity}
disabled={liveActivity.status === "loading"}
style={[
styles.liveActivityButton,
liveActivity.status === "active" && styles.liveActivityButtonActive,
]}
>
<Text style={styles.liveActivityButtonText}>
{liveActivity.status === "active"
? "🔴 列車追従 Live Activity 終了"
: liveActivity.status === "loading"
? "⏳ 開始中..."
: "🟢 列車追従 Live Activity 開始"}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
liveActivityButton: {
marginHorizontal: 10,
marginVertical: 6,
paddingVertical: 10,
paddingHorizontal: 16,
borderRadius: 10,
backgroundColor: "rgba(0, 153, 204, 0.12)",
borderWidth: 1,
borderColor: "rgba(0, 153, 204, 0.5)",
alignItems: "center",
},
liveActivityButtonActive: {
backgroundColor: "rgba(220, 50, 50, 0.1)",
borderColor: "rgba(220, 50, 50, 0.5)",
},
liveActivityButtonText: {
fontSize: 14,
fontWeight: "600",
color: "#0099cc",
},
});
@@ -1,5 +1,5 @@
import React, { FC } from "react";
import { ScrollView } from "react-native";
import { View } from "react-native";
import { TrainDataView } from "./TrainDataView";
import { trainDataType } from "@/lib/trainPositionTextArray";
import type { NavigateFunction } from "@/types";
@@ -18,12 +18,13 @@ export const LongHeader:FC<props> = ({
navigate,
}) => {
return (
<ScrollView
<View
style={{
flexDirection: "row",
flex: 1,
width: "100%",
alignSelf: "stretch",
}}
horizontal
pagingEnabled
>
<TrainDataView
currentTrainData={currentTrainData}
@@ -33,6 +34,6 @@ export const LongHeader:FC<props> = ({
navigate={navigate}
key={"LongHeader"}
/>
</ScrollView>
</View>
);
};
@@ -1,7 +1,8 @@
import { trainPosition, trainDataType } from "@/lib/trainPositionTextArray";
import React, { FC } from "react";
import { View, Text, TextStyle, ViewStyle } from "react-native";
import { View, Text, ViewStyle } from "react-native";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
type stateBox = {
currentTrainData: trainDataType | undefined;
@@ -23,6 +24,7 @@ export const PositionBox: FC<stateBox> = (props) => {
lineNumber,
} = props;
const { colors } = useThemeColors();
const { fontScale } = useResponsive();
let firstText = "";
let secondText = "";
let marginText = "";
@@ -49,11 +51,11 @@ export const PositionBox: FC<stateBox> = (props) => {
}
return (
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
<Text style={{ fontSize: fontScale(12), color: colors.textAccent }}>{title}</Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
{firstText && (
<Text style={[mode == 2 ? boxTextStyle2 : (isBetween ? boxTextStyle : boxTextStyleBig), { color: colors.textAccent }]}>
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : (isBetween ? { fontSize: fontScale(25), textAlign: "right" as const } : { fontSize: fontScale(28), textAlign: "right" as const }), { color: colors.textAccent }]}>
{firstText}
</Text>
)}
@@ -63,7 +65,7 @@ export const PositionBox: FC<stateBox> = (props) => {
</Text>
)}
{secondText && (
<Text style={[mode == 2 ? boxTextStyle2 :(isBetween ? boxTextStyle : boxTextStyleMini), { color: colors.textAccent }]}>
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } :(isBetween ? { fontSize: fontScale(25), textAlign: "right" as const } : { fontSize: fontScale(16), textAlign: "right" as const }), { color: colors.textAccent }]}>
{secondText}
</Text>
)}
@@ -72,8 +74,8 @@ export const PositionBox: FC<stateBox> = (props) => {
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
<Text
style={{
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
fontSize: 10,
...(mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }),
fontSize: fontScale(10),
color: colors.textAccent,
}}
>
@@ -97,23 +99,3 @@ const boxStyle2: ViewStyle = {
padding: 5,
margin: 5,
};
const boxTextStyle2: TextStyle = {
fontSize: 18,
textAlign: "right",
};
const boxTextStyleBig: TextStyle = {
fontSize: 28,
textAlign: "right",
};
const boxTextStyleMini: TextStyle = {
fontSize: 16,
textAlign: "right",
};
const boxTextStyle: TextStyle = {
fontSize: 25,
textAlign: "right",
};
@@ -1,10 +1,12 @@
import React from "react";
import { View, Text, LayoutAnimation, TouchableOpacity } from "react-native";
import { View, Text, TouchableOpacity } from "react-native";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
export const ScrollStickyContent = (props) => {
const { currentTrainData, showThrew, setShowThrew, haveThrough } = props;
const { colors } = useThemeColors();
const { fontScale } = useResponsive();
return (
<View
style={{
@@ -22,13 +24,13 @@ export const ScrollStickyContent = (props) => {
flex: 1,
}}
>
<Text style={{ fontSize: 20, color: colors.text }}></Text>
<Text style={{ fontSize: fontScale(20), color: colors.text }}></Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: "row" }}>
{!isNaN(currentTrainData?.delay) && currentTrainData?.delay != 0 && (
<Text
style={{
fontSize: 15,
fontSize: fontScale(15),
color: colors.text,
position: "absolute",
right: 110,
@@ -41,7 +43,7 @@ export const ScrollStickyContent = (props) => {
)}
<Text
style={{
fontSize: 20,
fontSize: fontScale(20),
color: isNaN(currentTrainData?.delay)
? colors.text
: currentTrainData?.delay == 0
@@ -55,17 +57,13 @@ export const ScrollStickyContent = (props) => {
<TouchableOpacity
onPress={() => {
if (!haveThrough) return;
LayoutAnimation.configureNext({
duration: 200,
update: { type: "easeInEaseOut", springDamping: 0.6 },
});
setShowThrew(!showThrew);
}}
>
<View style={{ flex: 1 }} />
<Text
style={{
fontSize: 12,
fontSize: fontScale(12),
width: 50,
paddingBottom: 0,
margin: "auto",
@@ -1,5 +1,5 @@
import React, { FC } from "react";
import { ScrollView } from "react-native";
import { View } from "react-native";
import { TrainDataView } from "./TrainDataView";
import { trainDataType } from "@/lib/trainPositionTextArray";
import type { NavigateFunction } from "@/types";
@@ -18,13 +18,13 @@ export const ShortHeader:FC<props> = ({
navigate,
}) => {
return (
<ScrollView
<View
style={{
flexDirection: "row",
flex: 1,
width: "100%",
alignSelf: "stretch",
}}
horizontal
pagingEnabled
>
<TrainDataView
mode={2}
@@ -35,6 +35,6 @@ export const ShortHeader:FC<props> = ({
navigate={navigate}
key={"ShortHeader"}
/>
</ScrollView>
</View>
);
};
@@ -1,6 +1,7 @@
import React, { CSSProperties, FC } from "react";
import { View, Text, StyleProp, TextStyle, ViewStyle } from "react-native";
import { View, Text, StyleProp, ViewStyle } from "react-native";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
type stateBox = {
text: string;
@@ -12,19 +13,20 @@ type stateBox = {
export const StateBox: FC<stateBox> = (props) => {
const { text, title, style, mode, endText } = props;
const { colors } = useThemeColors();
const { fontScale } = useResponsive();
return (
<View style={{ ...(mode == 2 ? boxStyle2 : boxStyle), backgroundColor: colors.surface, ...style }}>
<Text style={{ fontSize: 12, color: colors.textAccent }}>{title}</Text>
<Text style={{ fontSize: fontScale(12), color: colors.textAccent }}>{title}</Text>
<View style={{ flex: 1 }} />
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
<Text style={[mode == 2 ? boxTextStyle2 : boxTextStyle, { color: colors.textAccent }]}>{text}</Text>
<Text style={[mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }, { color: colors.textAccent }]}>{text}</Text>
</View>
{endText && (
<View style={{ flexDirection: mode == 2 ? "row" : "column" }}>
<Text
style={{
...{ ...(mode == 2 ? boxTextStyle2 : boxTextStyle) },
fontSize: 10,
...(mode == 2 ? { fontSize: fontScale(18), textAlign: "right" as const } : { fontSize: fontScale(25), textAlign: "right" as const }),
fontSize: fontScale(10),
color: colors.textAccent,
}}
>
@@ -47,12 +49,3 @@ const boxStyle2: ViewStyle = {
padding: 5,
margin: 5,
};
const boxTextStyle2: TextStyle = {
fontSize: 18,
textAlign: "right",
};
const boxTextStyle: TextStyle = {
fontSize: 25,
textAlign: "right",
};
@@ -1,5 +1,5 @@
import React, { useState, useEffect, FC } from "react";
import { View, TouchableOpacity, useWindowDimensions, Text } from "react-native";
import { View, TouchableOpacity, Text } from "react-native";
import { StateBox } from "./StateBox";
import { PositionBox } from "./PositionBox";
import { useDeviceOrientationChange } from "../../../stateBox/useDeviceOrientationChange";
@@ -13,6 +13,7 @@ import { useStationList } from "../../../stateBox/useStationList";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { customTrainDataDetector } from "@/components/custom-train-data";
import type { NavigateFunction } from "@/types";
import { stackAwareNavigate } from "@/lib/rootNavigation";
type props = {
@@ -33,7 +34,6 @@ export const TrainDataView:FC<props> = ({
}) => {
const { stationList } = useStationList();
const { width, height } = useWindowDimensions();
const { isLandscape } = useDeviceOrientationChange();
const { setInjectData } = useCurrentTrain();
@@ -155,7 +155,7 @@ export const TrainDataView:FC<props> = ({
flexDirection: "row",
//minHeight: 200,
//height: heightPercentageToDP("20%"),
width: isLandscape ? (width / 100) * 40 : width,
width: "100%",
flex: 1,
}}
>
@@ -164,16 +164,16 @@ export const TrainDataView:FC<props> = ({
//disabled={!onLine}
//onLongPress={openEditWindow}
onLongPress={()=>{
if (!onLine) return;
if (!currentTrainData) return;
setInjectData({ type:"train", value:currentTrainData?.num, fixed:true});
navigate("positions", { screen: "Apps" });
stackAwareNavigate("positions");
SheetManager.hide("EachTrainInfo");
}}
onPress={() => {
if (!onLine) return;
setInjectData({ type: "station", value: currentPosition[0], fixed: false });
navigate("positions", { screen: "Apps" });
stackAwareNavigate("positions");
SheetManager.hide("EachTrainInfo");
}}
>
@@ -22,8 +22,10 @@ import { ShowSpecialTrain } from "./EachTrainInfo/ShowSpecialTrain";
import { useTrainMenu } from "../../stateBox/useTrainMenu";
import { HeaderText } from "./EachTrainInfoCore/HeaderText";
import { useStationList } from "../../stateBox/useStationList";
import { useCurrentTrain } from "../../stateBox/useCurrentTrain";
import { useThemeColors } from "@/lib/theme";
import { useAllTrainDiagram } from "@/stateBox/useAllTrainDiagram";
import { useResponsive } from "@/lib/responsive";
// Custom hooks
import { useTrainDiagramData } from "./EachTrainInfoCore/hooks/useTrainDiagramData";
@@ -40,13 +42,16 @@ export const EachTrainInfoCore = ({
openStationACFromEachTrainInfo,
from,
navigate,
sheetOpened = false,
}) => {
const { stationList } = useStationList();
const { allCustomTrainData } = useAllTrainDiagram();
const { colors, fixed } = useThemeColors();
const { verticalScale, moderateScale } = useResponsive();
const { setTrainInfo } = useTrainMenu();
const { height } = useWindowDimensions();
const { isLandscape } = useDeviceOrientationChange();
const { getCurrentStationData } = useCurrentTrain();
const scrollRef = useRef<any>(null);
// Custom hooks for data management
@@ -70,7 +75,9 @@ export const EachTrainInfoCore = ({
} = useExtendedStations(trainData, setTrainData);
// UI state
const [showThrew, setShowThrew] = useState(false);
// 走行中の列車は初期状態から通過駅を表示する(後から showThrew を true に変更すると
// ActionSheet の onSheetLayout が再発火してスプリングアニメーションが途中でリスタートするため)
const [showThrew, setShowThrew] = useState(() => !!getCurrentStationData(data.trainNum));
const [isJumped, setIsJumped] = useState(false);
// Auto scroll to current position
@@ -80,7 +87,7 @@ export const EachTrainInfoCore = ({
scrollRef,
isJumped,
setIsJumped,
setShowThrew
sheetOpened
);
// Back button handler
@@ -124,7 +131,6 @@ export const EachTrainInfoCore = ({
} else {
SheetManager.hide("EachTrainInfo").then(() => {
setTimeout(() => {
// @ts-expect-error - SheetManager payload type is too restrictive
SheetManager.show("EachTrainInfo", { payload });
}, 200);
});
@@ -141,11 +147,11 @@ export const EachTrainInfoCore = ({
borderWidth: 1,
}}
>
<View style={{ height: 26, width: "100%" }}>
<View style={{ height: verticalScale(26), width: "100%" }}>
<View
style={{
height: 6,
width: 45,
height: verticalScale(6),
width: moderateScale(45),
borderRadius: 100,
backgroundColor: colors.borderLight,
marginVertical: 10,
@@ -172,7 +178,7 @@ export const EachTrainInfoCore = ({
scrollRef={scrollRef}
containerProps={{
style: {
maxHeight: isLandscape ? height - 94 : (height / 100) * 70,
maxHeight: height * 0.7,
backgroundColor:
customTrainType.data === "notService" ? "#777777ff" : colors.surface,
},
@@ -206,7 +212,7 @@ export const EachTrainInfoCore = ({
}
>
{customTrainType.data === "notService" && (
<Text style={{ backgroundColor: colors.surface, fontWeight: "bold" }}>
<Text style={{ backgroundColor: colors.surface, color: colors.text, fontWeight: "bold" }}>
</Text>
)}
@@ -1,4 +1,4 @@
import React, { FC, useMemo, useState } from "react";
import React, { FC, useMemo } from "react";
import { Text, View, TextStyle, TouchableOpacity } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { migrateTrainName } from "../../../lib/eachTrainInfoCoreLib/migrateTrainName";
@@ -16,6 +16,9 @@ import type { NavigateFunction } from "@/types";
import { useUnyohub } from "@/stateBox/useUnyohub";
import { useElesite } from "@/stateBox/useElesite";
import { useThemeColors } from "@/lib/theme";
import { formatWrappedTrainType, shouldWrapTrainType } from "@/lib/trainTypeWrap";
import { useResponsive } from "@/lib/responsive";
import { normalizeIconDisplayMode } from "@/lib/iconDisplayMode";
type Props = {
data: { trainNum: string; limited: string };
@@ -46,10 +49,11 @@ export const HeaderText: FC<Props> = ({
from,
scrollRef,
}) => {
const { limited, trainNum } = data;
const { trainNum } = data;
const { fixed } = useThemeColors();
const { updatePermission } = useTrainMenu();
const { fontScale } = useResponsive();
const { iconSetting } = useTrainMenu();
const { allCustomTrainData, getTodayOperationByTrainId } =
useAllTrainDiagram();
const { expoPushToken } = useNotification();
@@ -57,22 +61,22 @@ export const HeaderText: FC<Props> = ({
getUnyohubByTrainNumber,
getUnyohubEntriesByTrainNumber,
useUnyohub: unyohubEnabled,
unyohubData,
} = useUnyohub();
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
useElesite();
const iconDisplayMode = normalizeIconDisplayMode(iconSetting);
// 追加ソースのON/OFFをここで管理(将来ソースが増えたらここに足す)
const additionalSources = {
unyohub: unyohubEnabled,
elesite: elesiteEnabled,
};
const hasAdditionalSources = Object.values(additionalSources).some(Boolean);
// 列車名、種別、フォントの取得
const [
typeName,
trainName,
fontAvailable,
fontFamily,
isOneMan,
infogram,
priority,
@@ -93,7 +97,7 @@ export const HeaderText: FC<Props> = ({
to_data,
directions,
} = result;
const [typeString, fontAvailable, isOneMan] = getStringConfig(
const [typeString, fontFamily, isOneMan] = getStringConfig(
type,
trainNum,
);
@@ -105,7 +109,7 @@ export const HeaderText: FC<Props> = ({
(train_num_distance !== "" && !isNaN(parseInt(train_num_distance))
? ` ${parseInt(trainNum) - parseInt(train_num_distance)}`
: ""),
fontAvailable,
fontFamily,
isOneMan,
infogram,
priority,
@@ -118,7 +122,7 @@ export const HeaderText: FC<Props> = ({
return [
typeString,
"",
fontAvailable,
fontFamily,
isOneMan,
infogram,
priority,
@@ -130,8 +134,8 @@ export const HeaderText: FC<Props> = ({
case to_data && to_data !== "":
return [
typeString,
to_data + "行き",
fontAvailable,
`${to_data}行き`,
fontFamily,
isOneMan,
infogram,
priority,
@@ -144,9 +148,9 @@ export const HeaderText: FC<Props> = ({
return [
typeString,
migrateTrainName(
trainData[trainData.length - 1].split(",")[0] + "行き",
`${trainData[trainData.length - 1].split(",")[0]}行き`,
),
fontAvailable,
fontFamily,
isOneMan,
infogram,
priority,
@@ -156,11 +160,10 @@ export const HeaderText: FC<Props> = ({
result,
];
}
}, [trainData]);
}, [trainData, trainNum, allCustomTrainData]);
const todayOperation = getTodayOperationByTrainId(trainNum).filter(
(d) => d.state !== 100,
);
const allTodayOperation = getTodayOperationByTrainId(trainNum) ?? [];
const todayOperation = allTodayOperation.filter((d) => d.state !== 100);
let iconTrainDirection =
parseInt(trainNum.replace(/[^\d]/g, "")) % 2 == 0 ? true : false;
@@ -168,11 +171,34 @@ export const HeaderText: FC<Props> = ({
iconTrainDirection = directions ? true : false;
}
const unyohubFormation = getUnyohubByTrainNumber(trainNum);
const unyohubEntries = getUnyohubEntriesByTrainNumber(trainNum);
const unyohubLookupNum = customTrainData?.train_number_override || trainNum;
const isFreightRetsuban = unyohubLookupNum.includes("レ");
const unyohubTrainNumForSourceScreen = isFreightRetsuban
? unyohubLookupNum.replace(/レ/g, "")
: unyohubLookupNum;
const freightUnyohubCandidates = (() => {
const digits = unyohubTrainNumForSourceScreen.replace(/[^\d]/g, "");
const candidates = new Set<string>();
if (!digits) return candidates;
candidates.add(digits);
if (/^\d{2}$/.test(digits)) {
candidates.add(`30${digits}`);
candidates.add(`90${digits}`);
} else if (/^(30|90)\d{2}$/.test(digits)) {
candidates.add(digits.slice(-2));
}
return candidates;
})();
const unyohubFormation = getUnyohubByTrainNumber(unyohubLookupNum);
const unyohubEntries = isFreightRetsuban
? unyohubData.filter((unyo) =>
unyo.trains?.some(
(t) => !!t.train_number && freightUnyohubCandidates.has(t.train_number),
),
)
: getUnyohubEntriesByTrainNumber(unyohubTrainNumForSourceScreen);
const elesiteEntries = getElesiteEntriesByTrainNumber(trainNum);
// 車番(formations)が空でないエントリが1件以上あれば「運用Hub情報あり」と判定
const hasUnyohubFormation = unyohubEntries.some(
(e) => !!e.formations && e.formations.trim() !== "",
);
@@ -182,11 +208,20 @@ export const HeaderText: FC<Props> = ({
const hasExtraInfo =
priority > 200 ||
todayOperation?.length > 0 ||
todayOperation.length > 0 ||
hasUnyohubFormation ||
hasElesiteFormation;
const [isWrapped, setIsWrapped] = useState(false);
const wrapType = shouldWrapTrainType(typeName, trainName);
const openTrainInfoUrl = () => {
if (!trainInfoUrl) return;
const uri = trainInfoUrl.includes("pdf")
? getPDFViewURL(trainInfoUrl)
: trainInfoUrl;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
};
return (
<View
@@ -206,6 +241,7 @@ export const HeaderText: FC<Props> = ({
from={from}
todayOperation={todayOperation}
direction={iconTrainDirection}
iconDisplayMode={iconDisplayMode}
/>
<View
@@ -216,15 +252,23 @@ export const HeaderText: FC<Props> = ({
}}
>
<Text
numberOfLines={wrapType ? 2 : 1}
style={{
fontSize: 20,
fontSize: fontScale(20),
color: fixed.textOnPrimary,
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
fontWeight: !fontAvailable ? "bold" : undefined,
fontFamily,
fontWeight: fontFamily ? undefined : "bold",
marginRight: 5,
flexShrink: 0,
...(wrapType
? { width: fontScale(44), lineHeight: fontScale(22) }
: {}),
}}
>
{isWrapped ? typeName.replace(/(.{2})/g, "$1\n").trim() : typeName}
{wrapType ? formatWrappedTrainType(typeName) : typeName}
{fontFamily === "JR-WEST-PLUS" ? (
<Text style={{ fontFamily: "DiaPro", fontSize: 7 }}>{"\u00A0"}</Text>
) : null}
</Text>
{isOneMan && <OneManText />}
</View>
@@ -244,33 +288,25 @@ export const HeaderText: FC<Props> = ({
}
: {}),
}}
onPress={() => {
if (!trainInfoUrl) return;
const uri = trainInfoUrl.includes("pdf")
? getPDFViewURL(trainInfoUrl)
: trainInfoUrl;
navigate("generalWebView", { uri, useExitButton: true });
SheetManager.hide("EachTrainInfo");
}}
onPress={openTrainInfoUrl}
disabled={!trainInfoUrl}
>
<Text
style={{
...textConfig,
color: fixed.textOnPrimary,
...(trainName.length > 10 ? { fontSize: 16 } : {}),
...(trainName.length > 10
? { fontSize: fontScale(16) }
: { fontSize: fontScale(17) }),
flexShrink: 1,
}}
onTextLayout={(e) => {
if (e.nativeEvent.lines.length > 1) setIsWrapped(true);
}}
>
{trainName}
</Text>
<InfogramText infogram={infogram} />
</TouchableOpacity>
<View style={{ flex: 1 }} />
<Text style={{ ...textConfig, color: fixed.textOnPrimary }}>
<Text style={{ ...textConfig, fontSize: fontScale(17), color: fixed.textOnPrimary }}>
{showHeadStation.map((d) => `${headStation[d].id} + `)}
{trainNum}
{showTailStation.map((d) => ` + ${tailStation[d].id}`)}
@@ -285,6 +321,7 @@ export const HeaderText: FC<Props> = ({
(SheetManager.show as any)("TrainDataSources", {
payload: {
trainNum,
unyohubTrainNum: unyohubTrainNumForSourceScreen,
unyohubEntries,
elesiteEntries,
todayOperation,
@@ -301,7 +338,6 @@ export const HeaderText: FC<Props> = ({
},
});
} else {
// 追加ソースが全てオフ → 元の挙動(直接 DB ページを開く)
const uri = `https://jr-shikoku-data-system.pages.dev/trainData/${trainNum}?userID=${expoPushToken}&from=eachTrainInfo`;
navigate("generalWebView", { uri, useExitButton: false });
SheetManager.hide("EachTrainInfo");
@@ -3,13 +3,14 @@ import { Text } from "react-native";
import { useThemeColors } from "@/lib/theme";
type props = {
infogram: string;
fontSize?: number;
}
export const InfogramText: FC<props> = ({infogram}) => {
export const InfogramText: FC<props> = ({infogram, fontSize = 20}) => {
const { fixed } = useThemeColors();
return (
<Text
style={{
fontSize: 20,
fontSize,
color: fixed.textOnPrimary,
fontFamily: "JNR-font",
}}

Some files were not shown because too many files have changed in this diff Show More