217 Commits

Author SHA1 Message Date
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
18f11c88b1 WidgetSettingsコンポーネントを削除し、SettingTopPageのウィジェット設定リンクをAndroidプラットフォームに限定 2026-03-24 16:03:23 +00:00
harukinMBP
cee238d060 ShortcutWidgetのタイル表示を改善し、遅延情報と運行情報を取得する機能を追加 2026-03-25 00:34:34 +09:00
harukin-expo-dev-env
dad462ff45 ListViewとExGridSimpleViewに次の列車の時間帯へ自動スクロール機能を追加 2026-03-24 14:12:35 +00:00
harukin-expo-dev-env
26cde03d3e AllTrainDiagramViewのmeasure対象を外側コンテナに変更
検索エリア自体ではなく最外側Viewをmeasure対象にすることで、
iOS上でキーボード頭に入力ブロックが隠れる問題を修正。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 10:11:31 +00:00
harukin-expo-dev-env
0230b56f8b StationDiagramViewの外側Viewをmeasure対象に変更
fallback計算ではなく、外側コンテナのmeasure()で
キーボード上端までの正確なオフセットを算出するように修正。
検索バーがキーボードに埋もれる問題を解消。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 10:04:35 +00:00
harukin-expo-dev-env
a2912d77ae キーボード回避ロジックをuseKeyboardAvoid hookに共通化
3コンポーネントに重複していたキーボード処理を lib/useKeyboardAvoid.ts に集約:
- Androidの偽イベント(height<100)ガード+キャッシュ
- hide→show高速切替のデバウンス(100ms)
- Android measure()の150ms遅延
- LayoutAnimation easeInEaseOut

対象: AllTrainDiagramView, SearchUnitBox, StationDiagramView

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 09:56:51 +00:00
harukin-expo-dev-env
e80eeae211 StationDiagramViewにキーボード高さキャッシュ・デバウンス・ガードを適用
- height<100の偽イベントを無視し、lastValidKbでキャッシュ利用
- hideデバウンス100msで素早い閉じ→開きに対応
- Android 150ms遅延でmeasure安定化
- LayoutAnimationをeaseInEaseOutに統一

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 09:50:17 +00:00
harukin-expo-dev-env
33410fcd61 fix: キーボード表示時の測定処理をリファクタリングし、タイマー管理を追加 2026-03-24 09:02:18 +00:00
harukin-expo-dev-env
ff25363600 fix: キーボード表示時のアニメーションの持続時間を短縮し、レイアウト調整を改善 2026-03-24 08:58:45 +00:00
harukin-expo-dev-env
36cd7448a5 fix: 検索ユニットボックスでキーボード表示時の位置調整を改善し、親コンテナの参照を追加 2026-03-24 08:51:09 +00:00
harukin-expo-dev-env
4a0e252366 fix: SearchUnitBoxに親コンテナの参照を追加し、キーボード表示時の位置調整を改善 2026-03-24 08:43:04 +00:00
harukin-expo-dev-env
1c96776f56 fix: アイコンシェア機能の options 引数ミスを修正
shareAsync の第2引数が (options = {...}) という代入式になっており
グローバル変数への代入になっていた。正しいオブジェクトリテラルに修正。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 08:32:44 +00:00
harukin-expo-dev-env
53b95a91d5 fix: アイコンプレビューが表示されないバグを修正
Image コンポーネントに padding: 30 を直接設定していたため、
RN 0.76+ の新アーキテクチャでコンテンツ領域がゼロになり画像が非表示になっていた。
padding を持つ wrapper View に移動し、Image サイズを 50x50 から 80x80 に拡大。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 08:26:40 +00:00
harukin-expo-dev-env
82fe8d1244 fix: タブのスタック状態を管理し、追加画面の表示状態を更新 2026-03-24 06:46:11 +00:00
harukin-expo-dev-env
80eeb51134 fix: 各コンポーネントのスタイルとナビゲーションを改善し、パフォーマンスを向上 2026-03-24 06:39:37 +00:00
harukin-expo-dev-env
2d96bdcad9 fix: Felicaウィジェットにスキャンタイムスタンプを追加 2026-03-24 04:37:56 +00:00
harukin-expo-dev-env
59653bbc16 fix: ダークモードに対応し、背景色を動的に変更 2026-03-24 04:37:46 +00:00
harukin-expo-dev-env
f34d06192b fix: アプリ起動時の意図しない自動画面遷移バグを修正
- useNotifications: getLastNotificationResponseAsync の処理済み通知IDを
  AsyncStorage に永続化し、アプリ再起動後に同じ通知で再ナビゲーションするバグを修正
- App.tsx: getInitialURL で返るディープリンクURLを永続化し、Android の
  singleTask モードで古いURLが再処理されるバグを修正

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 04:07:54 +00:00
harukin-expo-dev-env
16bf96faf7 fix: 不要なコードを削除し、URL処理を簡素化 2026-03-24 04:02:58 +00:00
harukin-expo-dev-env
3925370b97 fix: 与島PAの座標を正確な位置に修正
Wikipedia掲載の公式座標を使用
北緯34.389472度 東経133.816444度(香川県坂出市与島町587番地)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 03:24:11 +00:00
harukin-expo-dev-env
dcd8de06f8 fix: 与島PAの座標を修正(本島→与島)
34.377355, 133.773046(本島町)→ 34.360016, 133.784882(与島PA)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 03:13:52 +00:00
harukin-expo-dev-env
1d57f2a5c6 fix: 通知折りたたみ時も走行区間を表示
- contentTextにbody全行を全角スペース区切りで表示(折りたたみ時も見える)
- pollTrainPositionの変化なしスキップを除去(常に通知を更新)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 02:54:24 +00:00
harukin-expo-dev-env
d3e4b173c7 fix: StationNumber: null のスポットによるクラッシュを修正
- CarouselBox: key と keyExtractor を null セーフに
- StationPagination: data が空の場合の early return と optional chaining
- FixedStationBox / FixedTrainBox / ListViewItem: StationNumber?.slice() に修正

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 02:11:17 +00:00
harukin-expo-dev-env
5202f35702 feat: 与島(観光スポット)をトップメニューに追加
- assets/originData/spots.ts: 与島PAデータを新規作成(isSpot: true, StationNumber: null)
- lib/CommonTypes.ts: StationProps に isSpot フラグを追加
- lib/getStationList.ts: 観光スポットキーとして stationList に追加
- stateBox/useStationList.tsx: StationNumber: null でも名前検索が通るよう修正、getInjectJavascriptAddress で路線外エントリをスキップ
- menu.tsx: 位置情報検索に観光スポットを追加
- components/観光スポット看板/SpotSign.tsx: テーマパーク風の観光スポット看板コンポーネントを新規作成
- components/Menu/Carousel/CarouselBox.tsx: isSpot フラグで SpotSign に切り替え
- components/Menu/Carousel/GridMiniSign.tsx: isSpot 対応・ドット除去表示
- components/StationDiagram/SearchBox/SearchInputSuggestBox.tsx: ドット除去表示
- components/StationDiagram/StationDiagramView.tsx: スポットの「駅」表記を除去

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 01:58:16 +00:00
harukin-expo-dev-env
dc3d250466 fix: 次駅表示をDirection非依存に修正(JS/Kotlin両方)
- JS側: currentPosition[0]ではなくstopStationIDList上のmax(idx0,idx1)で進行方向の駅を判定
- Kotlin側: pollRunnable復活、allStationsのダイヤ順でmaxOf(idx0,idx1)で向かう駅を判定
- Kotlin at-station: 停車中は現在駅を表示(JS側と統一)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-24 01:39:31 +00:00
harukin-expo-dev-env
3f6b3cfcfb fix: API呼び出しにタイムアウトを追加し、エラーハンドリングを改善 2026-03-24 00:19:19 +00:00
harukin-expo-dev-env
2cd5142262 fix: バックグラウンドでのデータ取得を継続し、列車追跡の終了条件をフォアグラウンドに依存させるよう修正 2026-03-23 15:52:23 +00:00
harukin-expo-dev-env
4b518b848e fix: FelicaQuickAccessWidgetとFelicaBalanceWidgetのUIを改善し、背景デザインを最適化 2026-03-23 15:14:08 +00:00
harukin-expo-dev-env
fb89a2b334 fix: Live Activity関連の不要なコードを削除し、トレイン情報の構築を最適化 2026-03-23 14:54:56 +00:00
harukin-expo-dev-env
a66af59438 fix: Live Activityの定期的な更新を追加し、selectedTrainの再計算を60秒ごとに実施 2026-03-23 11:45:08 +00:00
harukin-expo-dev-env
04d0f50b99 fix: Live Activityの自動開始条件をselectedTrainに依存させるように修正 2026-03-23 11:33:36 +00:00
harukin-expo-dev-env
f4b86f4e77 fix: FelicaQuickAccessWidgetおよびShortcutWidgetのUIを改善し、バランスウィジェットのデザインを更新 2026-03-23 11:26:17 +00:00
harukin-expo-dev-env
91470b5db8 fix: LiveActivityButtonを削除し、FixedStationおよびFixedTrainコンポーネントでの自動Live Activity開始を実装 2026-03-23 11:25:29 +00:00
harukin-expo-dev-env
ffcc6ff660 fix: 列車通知機能に路線色を追加し、進捗スタイルのセグメントに対応 2026-03-23 10:55:14 +00:00
harukin-expo-dev-env
681b12b622 fix: 通知機能のエラーハンドリングを改善し、Androidの通知権限を要求する処理を追加 2026-03-23 09:37:15 +00:00
harukin-expo-dev-env
13f2c4de7a feat: 駅固定モード バックグラウンド更新 + 通知書式改善
- 駅固定モードでもForeground Serviceのバックグラウンドポーリングを有効化
- pollStationTrains(): APIから遅延情報を取得し通知を自動更新
- テキスト書式を「00:00 特急 ○○号 ○○行 定刻」に変更
- StationTrainInfo に typeColor フィールド追加
- StationLockNotificationBuilder: 種別名にBackgroundColorSpan適用
- ExpoLiveActivityModule: 駅固定もMap引数方式に変更
- trainsJson/stationName をServiceに保存してバックグラウンドで使用

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 06:31:05 +00:00
harukin-expo-dev-env
f19600a3af fix: ProgressStyle Point位置計算を修正
- Point(0) は @IntRange(from=1) 違反 → coerceIn(1, max) で修正
- Segment length を均等固定値(100)にして丸め誤差を排除
- progressMax = numSegments * 100 で座標系を統一
- Point position = stationIndex * 100 で全駅等分に忠実に配置
- デバッグログにpointPositionsを追加(Metroコンソールで確認可能)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 06:21:26 +00:00
harukin-expo-dev-env
9a567d2486 debug: ProgressStyle にログ追加 + dimColor alpha を100に増加
- buildProgressStyle にデバッグログ追加(total, stops, curIdx, progressValue)
- build() にデータ到着ログ追加(allStations.size, isStop count)
- dimColor alpha を60→100に上げて未通過Pointの視認性向上
- mapIndexedNotNull → forEachIndexed + mutableList に変更(明示的)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 06:09:47 +00:00
harukin-expo-dev-env
9271629aa9 fix: ProgressStyle復元 + 進捗位置更新の修正
- ProgressStyle (Point/Segment) を復元して描画
- 停車駅のみPointを配置(通過駅はセグメントのみ)
- currentStationIndex の部分一致フォールバック追加
- currentPosition 依存を除去し train.Pos から直接計算
- bodyフォーマッタで currentStation の '~' 区間を正しく分離

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 05:44:30 +00:00
harukin-expo-dev-env
d79f5a07f8 feat: TrainDataSourcesコンポーネントにスクリーンショット共有機能を追加 2026-03-23 05:14:05 +00:00
harukin-expo-dev-env
86a4428861 feat: 通知進捗バー再設計 - 全駅対応の進捗表示
- 進捗バー: 小●=通過駅, 大●=停車駅, 🚃=現在地
- タイトル: ○○号 ○○行き
- サブタイトル: 次は ●● / △△~△△間走行中
- StationEntry (name, isStop) で全駅リスト(通過含む)を通知に渡す
- TrainFollowNotificationBuilder: allStationsJson対応のProgressStyle
- LiveActivityForegroundService: バックグラウンドポーリングでも全駅インデックス計算
- FixedTrainBox/LiveActivityButton: allStations, currentStationIndex を計算・送信

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-23 05:01:55 +00:00
harukin-expo-dev-env
960acdbb3f refactor: update ShortcutWidget layout and component structure for improved readability and maintainability 2026-03-23 04:40:02 +00:00
harukin-expo-dev-env
86123ecb81 feat: implement train tracking notifications with background polling and update UI components 2026-03-23 04:33:58 +00:00
harukin-expo-dev-env
1d14bcf91a feat: add iOS Live Activity APNs Push Notification implementation details 2026-03-23 01:33:12 +00:00
harukin-expo-dev-env
c7b1501475 fix: correct scroll reference in useAutoScroll hook 2026-03-23 01:32:54 +00:00
harukin-expo-dev-env
814de31418 feat: add date formatting and stale check for Unyohub entries in TrainDataSources 2026-03-23 01:10:59 +00:00
harukin-expo-dev-env
ecc9ee313e feat: update navigation handling and widget click actions for improved user experience 2026-03-22 23:23:32 +00:00
harukin-expo-dev-env
0a2333a201 feat: add Android notification permission handling and improve error logging for live notifications 2026-03-22 23:06:56 +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
0d45732d66 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
38171574e8 Merge commit 'ce4fb4d1dabf8c795518af66ca4b3326b63cb355' 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
ce4fb4d1da Merge commit '39a5b33e7748a6f6240225a98e09435e4cb4f469' into feature/migration-expo-upgrade 2026-03-22 17:09:51 +00:00
harukin-expo-dev-env
39a5b33e77 feat: add station progress tracking and notification updates for train follow feature 2026-03-22 17:09:19 +00:00
harukinMBP
6829744fa4 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
777b5c8acb 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
30e4e9780a fix: update activity state handling to use ActivityContent with nil staleDate 2026-03-22 23:09:13 +09:00
harukinMBP
d9574f991d 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
a665bf3a74 feat: add Live Activity support for station locking in StationDiagramView 2026-03-22 23:01:00 +09:00
harukinMBP
2cdcb5176b feat: add hooks for managing Live Activities for station locking and train following 2026-03-22 22:51:58 +09:00
harukinMBP
44cb462595 feat: add Live Activities support for train tracking and station locking 2026-03-22 22:49:05 +09:00
harukin-expo-dev-env
9bf7a735c1 feat: add androidGoogleMapsApiKey to app.json configuration 2026-03-22 13:17:53 +00:00
harukin-expo-dev-env
bdffce9e6a feat: add appleTeamId to iOS configuration in app.json 2026-03-22 11:48:59 +00:00
harukin-expo-dev-env
d4ad8c005e 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
0d0b82eee1 feat: FeliCa対応の可用性チェック機能を追加 2026-03-22 10:13:36 +00:00
harukinMBP
3e26463354 Update app.json build number to 58 and refine NFC error handling in ExpoFelicaReaderModule 2026-03-22 18:56:13 +09:00
harukinMBP
cefca15de9 Implement NFC scanning functionality in ExpoFelicaReader module 2026-03-21 22:04:55 +09:00
harukinMBP
62c84f153e Fix .gitignore: re-include modules/**/ios/ for EAS build, remove debug code 2026-03-21 20:13:34 +09:00
harukinMBP
83741e32fc Debug: patch resolveCommand.js and apple.js with logging, add pod fallback 2026-03-21 20:03:40 +09:00
harukinMBP
f262226d4c Enhanced autolinking debug: use external JS file 2026-03-21 19:46:25 +09:00
harukinMBP
032bcf127e Add Podfile debug plugin to trace autolinking resolve 2026-03-21 19:33:22 +09:00
harukinMBP
787718c36a Fix ExpoFelicaReader module: restore full definition, update diagnostics 2026-03-21 17:54:54 +09:00
harukinMBP
5457ced33d Add expo-felica-reader to dependencies for EAS build autolinking 2026-03-21 17:54:39 +09:00
harukin-expo-dev-env
91cad9c2c8 Refactor code structure for improved readability and maintainability 2026-03-20 07:14:58 +00:00
harukin-expo-dev-env
11cd8e0f40 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
7c84b037ac 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
676460353f 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
3f1da7272f 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
ea261f4bbb chore: 不要なビルドファイル build-1773815539430.apk を削除 2026-03-18 07:12:45 +00:00
harukin-expo-dev-env
e032dc9d70 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
265 changed files with 19150 additions and 2269 deletions

2
.gitignore vendored
View File

@@ -58,3 +58,5 @@ android/
ios/
!modules/**/ios/
*.ipa
*.apk
*.aab

113
App.tsx
View File

@@ -1,5 +1,10 @@
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 { GestureHandlerRootView } from "react-native-gesture-handler";
import "./utils/disableFontScaling"; // グローバルなフォントスケーリング無効化
import { AppContainer } from "./Apps";
@@ -10,7 +15,7 @@ import { CurrentTrainProvider } from "./stateBox/useCurrentTrain";
import { AreaInfoProvider } from "./stateBox/useAreaInfo";
import { BusAndTrainDataProvider } from "./stateBox/useBusAndTrainData";
import { AllTrainDiagramProvider } from "./stateBox/useAllTrainDiagram";
import { SheetProvider } from "react-native-actions-sheet";
import { SheetProvider, SheetManager } from "react-native-actions-sheet";
import "./components/ActionSheetComponents/sheets";
import { TrainDelayDataProvider } from "./stateBox/useTrainDelayData";
import { SafeAreaProvider } from "react-native-safe-area-context";
@@ -20,7 +25,7 @@ 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 { rootNavigationRef, stackAwareNavigate } from "./lib/rootNavigation";
import { AppThemeProvider } from "./lib/theme";
import StatusbarDetect from "./StatusbarDetect";
@@ -49,32 +54,65 @@ export default function App() {
return;
}
rootNavigationRef.navigate("topMenu", {
stackAwareNavigate("topMenu", {
screen: "setting",
params: {
screen: "FelicaHistoryPage",
},
} as any);
});
};
const navigateWhenReady = (
callback: () => void,
url: string,
retryCount = 0
) => {
if (!rootNavigationRef.isReady()) {
if (retryCount < 8) {
setTimeout(() => navigateWhenReady(callback, url, retryCount + 1), 250);
}
return;
}
callback();
};
const routeFromUrl = (url: string, retryCount = 0) => {
const normalized = (url || "").toLowerCase();
if (!normalized) return;
const isFelicaUrl =
if (
normalized.includes("felicahistorypage") ||
normalized.includes("open/felica");
if (!isFelicaUrl) return;
if (!rootNavigationRef.isReady()) {
if (retryCount < 8) {
setTimeout(() => routeFromUrl(url, retryCount + 1), 250);
}
return;
normalized.includes("open/felica")
) {
navigateWhenReady(() => openFelicaPage(), url, retryCount);
} else if (normalized.includes("open/traininfo")) {
navigateWhenReady(() => {
stackAwareNavigate("topMenu", { screen: "menu" });
setTimeout(() => {
SheetManager.show("JRSTraInfo");
}, 450);
}, url, retryCount);
} else if (normalized.includes("open/operation")) {
navigateWhenReady(() => {
stackAwareNavigate("information");
}, url, retryCount);
} else if (normalized.includes("open/settings")) {
navigateWhenReady(() => {
stackAwareNavigate("topMenu", {
screen: "setting",
});
}, url, retryCount);
} else if (normalized.includes("open/topmenu")) {
navigateWhenReady(() => {
stackAwareNavigate("topMenu", {
screen: "menu",
});
}, url, retryCount);
} else if (normalized.includes("positions/apps")) {
navigateWhenReady(() => {
stackAwareNavigate("positions");
}, url, retryCount);
}
openFelicaPage();
};
Linking.getInitialURL().then((url) => {
@@ -97,10 +135,10 @@ export default function App() {
StationListProvider,
FavoriteStationProvider,
TrainDelayDataProvider,
TrainMenuProvider, // CurrentTrainProvider より先に置くことで useTrainMenu が使える
CurrentTrainProvider,
AreaInfoProvider,
BusAndTrainDataProvider,
TrainMenuProvider,
SheetProvider,
]);
return (
@@ -108,13 +146,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>
);
}

View File

@@ -1,7 +1,7 @@
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { NavigationContainer, DarkTheme, DefaultTheme } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Platform, ActivityIndicator, View, useColorScheme } from "react-native";
import { Animated, Platform, ActivityIndicator, View, StyleSheet, StatusBar } from "react-native";
import { useNavigationState } from "@react-navigation/native";
import { useFonts } from "expo-font";
import { LinearGradient } from "expo-linear-gradient";
@@ -15,6 +15,8 @@ import lineColorList from "./assets/originData/lineColorList";
import { stationIDPair } from "./lib/getStationList";
import "./components/ActionSheetComponents/sheets";
import { rootNavigationRef } from "./lib/rootNavigation";
import { fixedColors } from "./lib/theme/colors";
import { useThemeColors } from "./lib/theme";
type RootTabParamList = {
positions: undefined;
@@ -36,6 +38,17 @@ const Tab = createBottomTabNavigator<RootTabParamList>();
export function AppContainer() {
const { areaInfo, areaIconBadgeText, isInfo } = useAreaInfo();
const { selectedLine } = useTrainMenu();
const [isExtraWindowOpen, setIsExtraWindowOpen] = React.useState(false);
// フェードアニメーション用 (0=通常, 1=追加ウィンドウ青)
const fadeAnim = React.useRef(new Animated.Value(0)).current;
React.useEffect(() => {
Animated.timing(fadeAnim, {
toValue: isExtraWindowOpen ? 1 : 0,
duration: 300,
useNativeDriver: false,
}).start();
}, [isExtraWindowOpen]);
const lineColor = selectedLine && stationIDPair[selectedLine]
? lineColorList[stationIDPair[selectedLine]]
@@ -48,17 +61,14 @@ 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 lineColorDark = lineColor ? darkenHex(lineColor, 0.78) : null;
const linking = {
prefixes: ["jrshikoku://"],
config: {
screens: {
positions: {
screens: {
Apps: "positions/apps",
},
screens: {},
},
topMenu: {
screens: {
@@ -107,8 +117,16 @@ export function AppContainer() {
);
}
return (
<NavigationContainer ref={rootNavigationRef} linking={linking}>
{/* @ts-expect-error - Tab.Navigator type definition issue */}
<NavigationContainer
ref={rootNavigationRef}
linking={linking}
theme={isDark ? DarkTheme : DefaultTheme}
onStateChange={(state) => {
const activeRoute = state?.routes?.[state?.index ?? 0];
const hasExtra = (activeRoute?.state?.index ?? 0) > 0;
setIsExtraWindowOpen(hasExtra);
}}
>
<Tab.Navigator
initialRouteName="topMenu"
screenOptions={({ route }) => {
@@ -118,21 +136,34 @@ export function AppContainer() {
const defaultInactive = isDark ? "#8e8e93" : "#8e8e93";
return {
lazy: false,
tabBarHideOnKeyboard: Platform.OS === "android",
animation: "shift",
tabBarActiveTintColor: showGradient ? "white" : defaultActive,
tabBarInactiveTintColor: showGradient ? "rgba(255,255,255,0.75)" : defaultInactive,
tabBarStyle: showGradient
? { backgroundColor: "transparent" }
: { backgroundColor: defaultBg },
tabBarBackground: showGradient ? () => (
<LinearGradient
colors={[lineColor!, lineColorDark!]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={{ flex: 1 }}
/>
) : undefined,
sceneContainerStyle: { backgroundColor: defaultBg },
tabBarActiveTintColor: (showGradient || isExtraWindowOpen) ? "white" : defaultActive,
tabBarInactiveTintColor: (showGradient || isExtraWindowOpen) ? "rgba(255,255,255,0.75)" : defaultInactive,
tabBarStyle: { backgroundColor: "transparent" },
tabBarBackground: () => (
<View style={{ flex: 1 }}>
{/* 路線カラー or デフォルト背景 */}
{showGradient ? (
<LinearGradient
colors={[lineColor!, lineColorDark!]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={{ ...StyleSheet.absoluteFillObject }}
/>
) : (
<View style={{ ...StyleSheet.absoluteFillObject, backgroundColor: defaultBg }} />
)}
{/* 追加ウィンドウ時の青グラデーション(フェードイン/アウト) */}
<Animated.View style={{ ...StyleSheet.absoluteFillObject, opacity: fadeAnim }}>
<LinearGradient
colors={[fixedColors.primary, fixedColors.primaryDark]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={{ flex: 1 }}
/>
</Animated.View>
</View>
),
};
}}
>

View File

@@ -1,9 +1,18 @@
import React, { CSSProperties } from "react";
import { Alert, BackHandler, View, ViewProps } from "react-native";
import React from "react";
import { Alert, ActivityIndicator, AppState, AppStateStatus, BackHandler, StyleSheet, Text, TouchableOpacity, View } from "react-native";
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 {
DEFAULT_JR_DATA_SYSTEM_ENV,
normalizeJrDataSystemEnvironment,
rewriteJrDataSystemUrl,
} from "@/lib/jrDataSystemEnvironment";
export default ({ route }) => {
if (!route.params) {
return null;
@@ -13,6 +22,88 @@ export default ({ route }) => {
const { fixed } = useThemeColors();
const webViewRef = React.useRef<WebView>(null);
const [canGoBack, setCanGoBack] = React.useState(false);
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);
// バックグラウンド移行時刻の記録
const backgroundedAt = React.useRef<number | null>(null);
// 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);
setSelectedEnvironment(nextEnvironment);
setResolvedUri(
rewriteJrDataSystemUrl(
typeof uri === "string" ? uri : "",
nextEnvironment,
),
);
setIsEnvironmentReady(true);
};
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then(applyEnvironment)
.catch(() => applyEnvironment(DEFAULT_JR_DATA_SYSTEM_ENV));
return () => {
isMounted = false;
};
}, [uri]);
const handleReload = () => {
lastPongAt.current = Date.now();
setHasError(false);
hasErrorRef.current = false;
setIsLoading(true);
isLoadingRef.current = true;
setWebViewKey((k) => k + 1);
};
// AppState監視: バックグラウンド10秒超で復帰したらWebViewを再マウント
React.useEffect(() => {
const onAppStateChange = (nextState: AppStateStatus) => {
if (nextState.match(/inactive|background/)) {
backgroundedAt.current = Date.now();
} else if (nextState === "active" && backgroundedAt.current !== null) {
const elapsed = Date.now() - backgroundedAt.current;
backgroundedAt.current = null;
if (elapsed > 10_000) {
remount();
}
}
};
const subscription = AppState.addEventListener("change", onAppStateChange);
return () => subscription.remove();
}, [remount]);
useFocusEffect(
React.useCallback(() => {
@@ -31,27 +122,171 @@ export default ({ route }) => {
);
return (
<View style={{ height: "100%", backgroundColor: fixed.primary }}>
<WebView
source={{ uri }}
allowsBackForwardNavigationGestures
ref={webViewRef}
onNavigationStateChange={(navState) => {
{isEnvironmentReady && (
<WebView
key={webViewKey}
source={{ uri: resolvedUri }}
contentMode="mobile"
allowsBackForwardNavigationGestures
ref={webViewRef}
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;
}
const rewrittenUrl = rewriteJrDataSystemUrl(
request.url,
selectedEnvironment,
);
if (rewrittenUrl !== request.url) {
setResolvedUri(rewrittenUrl);
return false;
}
return true;
}}
onNavigationStateChange={(navState) => {
setCanGoBack(navState.canGoBack);
// SPA内遷移中は白画面誤検知を防ぐためblankCountをリセット
if (navState.loading) blankCountRef.current = 0;
if (navState.url === "https://unyohub.2pd.jp/integration/succeeded.php") {
goBack();
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
webViewRef.current?.goBack();
if (!hasAlerted.current) {
hasAlerted.current = true;
Alert.alert("鉄道運用HUBへの投稿完了", "運用HUBからのこのアプリへのデータ反映には暫く時間がかかりますので、しばらくお待ちください。", [
{ text: "完了" },
]);
}
}
}}
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="閉じる" />}
onMessage={(event) => {
const { data } = event.nativeEvent;
const parsed = JSON.parse(data);
const { type } = parsed;
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 === "back") return webViewRef.current?.goBack();
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,
},
});

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useRef, useState } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useWindowDimensions, Platform } from "react-native";
import { useWindowDimensions, Platform, useColorScheme } from "react-native";
import Constants from "expo-constants";
import { useResponsive } from "@/lib/responsive";
import { Dimensions, StatusBar } from "react-native";
@@ -27,9 +28,12 @@ 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 isDark = useColorScheme() === "dark";
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
useEffect(() => {
AS.getItem(STORAGE_KEYS.START_PAGE)
.then((res) => {
@@ -63,8 +67,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]);
@@ -80,9 +84,16 @@ export function MenuPage() {
setMapFullHeight(MapFullHeight);
}, [height, tabBarHeight, width]);
useEffect(() => {
const unsubscribe = addListener("tabPress", (e) => {
const unsubscribe = addListener("tabPress", (e: any) => {
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);
@@ -102,8 +113,17 @@ export function MenuPage() {
return unsubscribe;
}, [navigation]);
const stackNavRef = useRef<any>(null);
return (
<Stack.Navigator id={null}>
<Stack.Navigator
id={null}
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
screenListeners={({ navigation: stackNav }) => {
stackNavRef.current = stackNav;
return {};
}}
>
<Stack.Screen
name="menu"
options={{

View File

@@ -175,7 +175,63 @@ export const API_ENDPOINTS = {
};
```
## 📱 ビルド
## <EFBFBD> 外部からのアプリ起動(ディープリンク)
本アプリは複数の方法で外部から起動・画面遷移できます。
### カスタム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/`
## <20>📱 ビルド
```bash
# APKビルドAndroid

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;

29
Top.tsx
View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useNavigation } from "@react-navigation/native";
import { useColorScheme } from "react-native";
import Apps from "./components/Apps";
import TrainBase from "./components/trainbaseview";
import HowTo from "./howto";
@@ -15,10 +16,13 @@ import { news } from "./config/newsUpdate";
import { Linking, Platform } from "react-native";
import GeneralWebView from "./GeneralWebView";
import { StationDiagramView } from "@/components/StationDiagram/StationDiagramView";
import { positionsStackNavRef } from "./lib/rootNavigation";
const Stack = createStackNavigator();
export const Top = () => {
const { webview } = useCurrentTrain();
const { navigate, addListener, isFocused } = useNavigation();
const { navigate, addListener, isFocused } = useNavigation<any>();
const isDark = useColorScheme() === "dark";
const bgColor = isDark ? "#1c1c1e" : "#ffffff";
//地図用
const { mapSwitch } = useTrainMenu();
@@ -35,14 +39,22 @@ export const Top = () => {
return unsubscribe;
}, []);
const goToTrainMenu = useCallback(() => {
const stackNavRef = positionsStackNavRef;
const goToTrainMenu = useCallback((e: any) => {
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 (!isFocused()) return;
const stackNav = stackNavRef.current;
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;
@@ -54,7 +66,14 @@ export const Top = () => {
}, [addListener, goToTrainMenu]);
return (
<Stack.Navigator id={null}>
<Stack.Navigator
id={null}
screenOptions={{ cardStyle: { backgroundColor: bgColor } }}
screenListeners={({ navigation: stackNav }) => {
stackNavRef.current = stackNav;
return {};
}}
>
<Stack.Screen
name="Apps"
options={{

476
app.json
View File

@@ -24,7 +24,7 @@
"**/*"
],
"ios": {
"buildNumber": "58",
"buildNumber": "63",
"supportsTablet": true,
"bundleIdentifier": "jrshikokuinfo.xprocess.hrkn",
"appleTeamId": "54CRDT797G",
@@ -39,7 +39,10 @@
],
"ITSAppUsesNonExemptEncryption": false,
"NSSupportsLiveActivities": true,
"NSSupportsLiveActivitiesFrequentUpdates": true
"NSSupportsLiveActivitiesFrequentUpdates": true,
"UIBackgroundModes": [
"audio"
]
},
"entitlements": {
"com.apple.developer.nfc.readersession.formats": [
@@ -105,6 +108,7 @@
},
"plugins": [
"./plugins/with-android-local-properties",
"./plugins/with-nfc-widget-guard",
"@bacons/apple-targets",
[
"expo-font",
@@ -140,17 +144,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 +154,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 +164,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 +174,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 +503,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"
}
}
]
],
@@ -537,4 +979,4 @@
]
]
}
}
}

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

BIN
assets/icons/ef210a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
assets/icons/ef210l3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
assets/icons/ef210n.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
assets/icons/ef210n1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

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") },
]
}
}

BIN
assets/icons/s1200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
assets/icons/s1201.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
assets/icons/s150001to.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
assets/icons/s1501.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
assets/icons/s1550.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
assets/icons/s1551.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
assets/icons/s185ap1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
assets/icons/s185cm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
assets/icons/s185g.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
assets/icons/s185mm2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
assets/icons/s185tu_uzu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
assets/icons/s185ym2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

BIN
assets/icons/s2000-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
assets/icons/s2000_uwa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
assets/icons/s2000ganp1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/icons/s2000nl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
assets/icons/s2600apb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
assets/icons/s2600apr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
assets/icons/s2700_asi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
assets/icons/s2700_smn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
assets/icons/s2700_uzu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
assets/icons/s2700apr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
assets/icons/s2700apy1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/icons/s32kpuy1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/icons/s32kpuy2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
assets/icons/s32ns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
assets/icons/s32oni1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

BIN
assets/icons/s32oni1k.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
assets/icons/s32s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
assets/icons/s32smek1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

BIN
assets/icons/s32thtk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
assets/icons/s32to4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
assets/icons/s32to_ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
assets/icons/s3600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
assets/icons/s40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
assets/icons/s54nany1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
assets/icons/s54nany2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
assets/icons/s54s.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
assets/icons/s54to0ys.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
assets/icons/s6000f.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
assets/icons/s7001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
assets/icons/s8001nr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
assets/icons/s9000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
assets/icons/tosa9640ht.png Normal file

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

BIN
assets/icons/w141jg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
assets/icons/w40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
assets/icons/w741.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -0,0 +1,21 @@
/**
* 観光スポットデータ
* StationNumber は "SP" プレフィックスで管理(路線とは別系統)
* Station_JP の先頭ドット(.)はダイヤデータのキーと一致させるための命名規則
*/
export default [
{
Station_JP: ".与島",
Station_EN: "Yoshima",
MyStation: "0",
StationNumber: null,
DispNum: "3",
StationTimeTable: "",
StationMap: "https://www.google.co.jp/maps/place/34.389472,133.816444",
JrHpUrl: "https://www.jb-honshi.co.jp/yoshimapa/",
jslodApi: "spot",
lat: 34.389472,
lng: 133.816444,
isSpot: true,
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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,
@@ -197,7 +199,7 @@ export const EachStopList: FC<props> = ({
>
<View
style={{
width: 35,
width: moderateScale(35),
position: "relative",
marginHorizontal: 15,
flexDirection: "row",
@@ -222,7 +224,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,7 +249,7 @@ 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",
@@ -308,20 +310,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 +344,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 +359,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 +379,7 @@ const StationNumbersBox: FC<{ stn: string; se: seTypes }> = (props) => {
style={{
color: fixed.textOnPrimary,
textAlign: "center",
fontSize: 10,
fontSize: fontScale(10),
fontWeight: "bold",
}}
>
@@ -400,6 +404,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 +413,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",
}}
>

View File

@@ -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>
);
};

View File

@@ -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",
};

View File

@@ -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",

View File

@@ -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>
);
};

View File

@@ -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",
};

View File

@@ -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");
}}
>

View File

@@ -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";
@@ -33,7 +35,6 @@ import { useStopStationIDs } from "./EachTrainInfoCore/hooks/useStopStationIDs";
import { useTrainPosition } from "./EachTrainInfoCore/hooks/useTrainPosition";
import { useAutoScroll } from "./EachTrainInfoCore/hooks/useAutoScroll";
import { useExtendedStations } from "./EachTrainInfoCore/hooks/useExtendedStations";
import { LiveActivityButton } from "./EachTrainInfo/LiveActivityButton";
export const EachTrainInfoCore = ({
actionSheetRef,
@@ -41,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
@@ -71,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
@@ -81,7 +87,7 @@ export const EachTrainInfoCore = ({
scrollRef,
isJumped,
setIsJumped,
setShowThrew
sheetOpened
);
// Back button handler
@@ -125,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);
});
@@ -142,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,
@@ -173,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,12 +211,8 @@ export const EachTrainInfoCore = ({
/>
}
>
<LiveActivityButton
currentTrainData={currentTrainData}
currentPosition={currentPosition}
/>
{customTrainType.data === "notService" && (
<Text style={{ backgroundColor: colors.surface, fontWeight: "bold" }}>
<Text style={{ backgroundColor: colors.surface, color: colors.text, fontWeight: "bold" }}>
</Text>
)}

View File

@@ -16,6 +16,7 @@ import type { NavigateFunction } from "@/types";
import { useUnyohub } from "@/stateBox/useUnyohub";
import { useElesite } from "@/stateBox/useElesite";
import { useThemeColors } from "@/lib/theme";
import { useResponsive } from "@/lib/responsive";
type Props = {
data: { trainNum: string; limited: string };
@@ -49,6 +50,7 @@ export const HeaderText: FC<Props> = ({
const { limited, trainNum } = data;
const { fixed } = useThemeColors();
const { fontScale } = useResponsive();
const { updatePermission } = useTrainMenu();
const { allCustomTrainData, getTodayOperationByTrainId } =
useAllTrainDiagram();
@@ -57,6 +59,7 @@ export const HeaderText: FC<Props> = ({
getUnyohubByTrainNumber,
getUnyohubEntriesByTrainNumber,
useUnyohub: unyohubEnabled,
unyohubData,
} = useUnyohub();
const { getElesiteEntriesByTrainNumber, useElesite: elesiteEnabled } =
useElesite();
@@ -156,11 +159,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 +170,35 @@ 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情報あり」と判定
// 車番(formations) がある場合のみ「運用Hub情報あり」と判定
const hasUnyohubFormation = unyohubEntries.some(
(e) => !!e.formations && e.formations.trim() !== "",
);
@@ -197,7 +223,7 @@ export const HeaderText: FC<Props> = ({
width: "100%",
}}
onTouchStart={() =>
scrollRef.current?.current?.scrollTo({ y: 0, animated: true })
scrollRef.current?.scrollTo({ y: 0, animated: true })
}
>
<TrainIconStatus
@@ -217,7 +243,7 @@ export const HeaderText: FC<Props> = ({
>
<Text
style={{
fontSize: 20,
fontSize: fontScale(20),
color: fixed.textOnPrimary,
fontFamily: fontAvailable ? "JR-Nishi" : undefined,
fontWeight: !fontAvailable ? "bold" : undefined,
@@ -258,7 +284,7 @@ export const HeaderText: FC<Props> = ({
style={{
...textConfig,
color: fixed.textOnPrimary,
...(trainName.length > 10 ? { fontSize: 16 } : {}),
...(trainName.length > 10 ? { fontSize: fontScale(16) } : { fontSize: fontScale(17) }),
flexShrink: 1,
}}
onTextLayout={(e) => {
@@ -266,11 +292,11 @@ export const HeaderText: FC<Props> = ({
}}
>
{trainName}
<InfogramText infogram={infogram} />
</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 +311,7 @@ export const HeaderText: FC<Props> = ({
(SheetManager.show as any)("TrainDataSources", {
payload: {
trainNum,
unyohubTrainNum: unyohubTrainNumForSourceScreen,
unyohubEntries,
elesiteEntries,
todayOperation,

View File

@@ -1,5 +1,4 @@
import React,{ useEffect, MutableRefObject } from 'react';
import { LayoutAnimation, ScrollView } from 'react-native';
import { useEffect, MutableRefObject } from 'react';
export const useAutoScroll = (
@@ -8,24 +7,15 @@ export const useAutoScroll = (
scrollRef: MutableRefObject<any>,
isJumped: boolean,
setIsJumped: (value: boolean) => void,
setShowThrew: (value: boolean) => void
sheetOpened: boolean = false
) => {
useEffect(() => {
if (isJumped || !points?.length || !scrollRef) return;
// ActionSheetのスプリングアニメーション完了後まで待機
if (!sheetOpened || isJumped || !points?.length || !scrollRef) return;
const currentPositionIndex = points.findIndex((d) => d === true);
if (currentPositionIndex === -1) return;
setShowThrew(true);
const isPassingThrough = trainDataWithThrough[currentPositionIndex]?.split(',')[1] === '通過';
if (isPassingThrough) {
LayoutAnimation.configureNext({
duration: 400,
update: { type: 'easeInEaseOut', springDamping: 0.6 },
});
}
// 5駅以内の場合はスクロールしない
if (currentPositionIndex < 5) {
setIsJumped(true);
@@ -33,9 +23,11 @@ export const useAutoScroll = (
}
const scrollPosition = currentPositionIndex * 44 - 50;
setTimeout(() => {
scrollRef.current?.current?.scrollTo({ y: scrollPosition, animated: true });
const timer = setTimeout(() => {
scrollRef.current?.scrollTo({ y: scrollPosition, animated: true });
setIsJumped(true);
}, 400);
}, [points, trainDataWithThrough, scrollRef, isJumped, setIsJumped, setShowThrew]);
}, 100);
return () => clearTimeout(timer);
}, [sheetOpened, points, trainDataWithThrough, scrollRef, isJumped, setIsJumped]);
};

View File

@@ -1,25 +1,25 @@
import { useState, useEffect } from 'react';
import { useStationList } from '@/stateBox/useStationList';
const computeStopStationIDs = (data: string[], stationList: any[][]): string[][] =>
data.map((item) => {
const [stationName] = item.split(',');
return stationList
.map((lineStations) => lineStations.filter((s) => s.StationName === stationName))
.reduce((acc, s) => acc.concat(s), [])
.map((s) => s.StationNumber);
});
export const useStopStationIDs = (trainDataWithThrough: string[]) => {
const { stationList } = useStationList();
const [stopStationIDList, setStopStationIDList] = useState<string[][]>([]);
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
const [stopStationIDList, setStopStationIDList] = useState<string[][]>(() =>
computeStopStationIDs(trainDataWithThrough, stationList)
);
useEffect(() => {
const stationIDs = trainDataWithThrough.map((item) => {
const [stationName] = item.split(',');
const matchingStations = stationList
.map((lineStations) =>
lineStations.filter((station) => station.StationName === stationName)
)
.reduce((acc, stations) => acc.concat(stations), [])
.map((station) => station.StationNumber);
return matchingStations;
});
setStopStationIDList(stationIDs);
setStopStationIDList(computeStopStationIDs(trainDataWithThrough, stationList));
}, [trainDataWithThrough, stationList]);
return stopStationIDList;

View File

@@ -2,107 +2,114 @@ import { useState, useEffect } from 'react';
import { lineListPair, stationIDPair } from '@/lib/getStationList';
import { useStationList } from '@/stateBox/useStationList';
export const useThroughStations = (trainData) => {
const { originalStationList, stationList } = useStationList();
const [trainDataWithThrough, setTrainDataWithThrough] = useState([]);
const [haveThrough, setHaveThrough] = useState(false);
const computeThroughStations = (
trainData: string[],
stationList: any[][],
originalStationList: Record<string, any[]>
): { trainDataWithThrough: string[]; haveThrough: boolean } => {
if (!trainData.length) return { trainDataWithThrough: [], haveThrough: false };
useEffect(() => {
if (!trainData.length) {
setTrainDataWithThrough([]);
return;
let haveThrough = false;
const isCancel: boolean[] = [];
const stopStationList = trainData.map((item, index, array) => {
const [station, se] = item.split(',');
const [, nextSe] = array[index + 1]?.split(',') || [];
if (nextSe) {
// 運休判定ロジック:
// 1. 両方が休系(休編、休発、休着など)→ 運休区間
// 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行
// 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行
// 4. その他の休の組み合わせ → 運休区間
const bothCanceled = se.includes('休') && nextSe.includes('休');
const normalArrivalToSuspendStart =
(se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発'));
const suspendEndToNormalDeparture =
(se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編');
isCancel.push(bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture);
}
const isCancel = [];
const stopStationList = trainData.map((item, index, array) => {
const [station, se] = item.split(',');
const [, nextSe] = array[index + 1]?.split(',') || [];
if (se === '通編') haveThrough = true;
if (nextSe) {
// 運休判定ロジック:
// 1. 両方が休系(休編、休発、休着など)→ 運休区間
// 2. 着/着編 → 休発/休発編:到着後に運休開始 → 通過駅は通常運行
// 3. 休着/休着編 → 発/発編:運休終了後に出発 → 通過駅は通常運行
// 4. その他の休の組み合わせ → 運休区間
const bothCanceled = se.includes('休') && nextSe.includes('休');
const normalArrivalToSuspendStart =
(se === '着' || se === '着編') && (nextSe.includes('休') && nextSe.includes('発'));
const suspendEndToNormalDeparture =
(se.includes('休') && se.includes('着')) && (nextSe === '発' || nextSe === '発編');
const isCanceled = bothCanceled && !normalArrivalToSuspendStart && !suspendEndToNormalDeparture;
isCancel.push(isCanceled);
return stationList.map((a) => a.filter((d) => d.StationName === station));
});
const allThroughStationList = stopStationList.map((firstItem, index, array) => {
if (index === array.length - 1) return [];
const secondItem = array[index + 1];
let betweenStationLine = '';
let baseStationNumberFirst = '';
let baseStationNumberSecond = '';
Object.keys(stationIDPair).forEach((lineName, lineIndex) => {
if (!lineName) return;
const haveFirst = firstItem[lineIndex];
const haveSecond = secondItem[lineIndex];
if (haveFirst?.length && haveSecond?.length) {
betweenStationLine = lineName;
baseStationNumberFirst = haveFirst[0].StationNumber;
baseStationNumberSecond = haveSecond[0].StationNumber;
}
if (se === '通編') setHaveThrough(true);
return stationList.map((a) => a.filter((d) => d.StationName === station));
});
const allThroughStationList = stopStationList.map((firstItem, index, array) => {
if (index === array.length - 1) return [];
if (!betweenStationLine) return [];
const secondItem = array[index + 1];
let betweenStationLine = '';
let baseStationNumberFirst = '';
let baseStationNumberSecond = '';
const allThroughStation: string[] = [];
let reverse = false;
Object.keys(stationIDPair).forEach((lineName, lineIndex) => {
if (!lineName) return;
const haveFirst = firstItem[lineIndex];
const haveSecond = secondItem[lineIndex];
originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => {
const throughStatus = isCancel[index] ? '通休編' : '通過';
if (haveFirst?.length && haveSecond?.length) {
betweenStationLine = lineName;
baseStationNumberFirst = haveFirst[0].StationNumber;
baseStationNumberSecond = haveSecond[0].StationNumber;
}
});
if (!betweenStationLine) return [];
const allThroughStation = [];
let reverse = false;
originalStationList[lineListPair[stationIDPair[betweenStationLine]]]?.forEach((station) => {
const throughStatus = isCancel[index] ? '通休編' : '通過';
if (
station.StationNumber > baseStationNumberFirst &&
station.StationNumber < baseStationNumberSecond
) {
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
setHaveThrough(true);
reverse = false;
} else if (
station.StationNumber < baseStationNumberFirst &&
station.StationNumber > baseStationNumberSecond
) {
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
setHaveThrough(true);
reverse = true;
}
});
if (reverse) allThroughStation.reverse();
return allThroughStation;
if (
station.StationNumber > baseStationNumberFirst &&
station.StationNumber < baseStationNumberSecond
) {
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
haveThrough = true;
reverse = false;
} else if (
station.StationNumber < baseStationNumberFirst &&
station.StationNumber > baseStationNumberSecond
) {
allThroughStation.push(`${station.Station_JP},${throughStatus},`);
haveThrough = true;
reverse = true;
}
});
let mainArray = [...trainData];
let offset = 0;
if (reverse) allThroughStation.reverse();
return allThroughStation;
});
trainData.forEach((_, index) => {
offset += 1;
const throughStations = allThroughStationList[index];
if (!throughStations?.length) return;
let mainArray = [...trainData];
let offset = 0;
mainArray.splice(offset, 0, ...throughStations);
offset += throughStations.length;
});
trainData.forEach((_, index) => {
offset += 1;
const throughStations = allThroughStationList[index];
if (!throughStations?.length) return;
mainArray.splice(offset, 0, ...throughStations);
offset += throughStations.length;
});
setTrainDataWithThrough(mainArray);
return { trainDataWithThrough: mainArray, haveThrough };
};
export const useThroughStations = (trainData) => {
const { originalStationList, stationList } = useStationList();
// 初回レンダリング時に同期的に計算することでActionSheetのアニメーション中の高さ変化を防ぐ
const [state, setState] = useState(() =>
computeThroughStations(trainData, stationList, originalStationList)
);
useEffect(() => {
setState(computeThroughStations(trainData, stationList, originalStationList));
}, [trainData, stationList, originalStationList]);
return { trainDataWithThrough, haveThrough };
return { trainDataWithThrough: state.trainDataWithThrough, haveThrough: state.haveThrough };
};

View File

@@ -2,28 +2,34 @@ import { useState, useEffect } from 'react';
import { useAllTrainDiagram } from '@/stateBox/useAllTrainDiagram';
import { searchSpecialTrain } from '@/lib/eachTrainInfoCoreLib/searchSpecialTrain';
const parseTrainData = (trainNum: string, trainList: Record<string, string>) => {
if (!trainNum) return { data: [], trueIDs: [] };
const TD = trainList[trainNum];
if (!TD) {
const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList);
return { data: [], trueIDs: specialTrainActualIDs || [] };
}
return { data: TD.split('#').filter((d) => d !== ''), trueIDs: [] };
};
export const useTrainDiagramData = (trainNum) => {
const { allTrainDiagram: trainList } = useAllTrainDiagram();
const [trainData, setTrainData] = useState([]);
const [trueTrainID, setTrueTrainID] = useState([]);
const [isManuallyExtended, setIsManuallyExtended] = useState(false);
// 初回レンダリング時にコンテキストから同期的にデータを取得することで
// ActionSheetのアニメーション中に高さが変わるのを防ぐ
const [trainData, setTrainData] = useState(() => parseTrainData(trainNum, trainList).data);
const [trueTrainID, setTrueTrainID] = useState(() => parseTrainData(trainNum, trainList).trueIDs);
useEffect(() => {
if (!trainNum) return;
// 手動で拡張されている場合は上書きしない
if (isManuallyExtended) return;
const TD = trainList[trainNum];
if (!TD) {
const specialTrainActualIDs = searchSpecialTrain(trainNum, trainList);
setTrueTrainID(specialTrainActualIDs || []);
setTrainData([]);
return;
}
setTrainData(TD.split('#').filter((d) => d !== ''));
const { data, trueIDs } = parseTrainData(trainNum, trainList);
setTrueTrainID(trueIDs);
setTrainData(data);
}, [trainNum, trainList, isManuallyExtended]);
const setTrainDataExtended = (data) => {

View File

@@ -25,7 +25,7 @@ export const useTrainPosition = (
const trainPosData = getCurrentStationData(trainNum);
if (trainPosData) {
logger.debug('Train position data:', trainPosData);
setCurrentTrainData(trainPosData);
setCurrentTrainData(trainPosData as TrainData);
}
}, [currentTrain, trainNum, getCurrentStationData]);

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