diff --git a/src/components/RouteList.tsx b/src/components/RouteList.tsx index 561afd887..d8e19a3bc 100644 --- a/src/components/RouteList.tsx +++ b/src/components/RouteList.tsx @@ -91,7 +91,6 @@ export const RouteList = ({ style={{ width: '100%', height: '100%', - maxHeight: '50%', alignSelf: 'center', borderColor: isLEDTheme ? '#fff' : '#aaa', borderWidth: 1, diff --git a/src/components/RouteListModal.tsx b/src/components/RouteListModal.tsx index e9346f016..6c5f3d1eb 100644 --- a/src/components/RouteListModal.tsx +++ b/src/components/RouteListModal.tsx @@ -1,7 +1,7 @@ import React from 'react' import { ActivityIndicator, Modal, StyleSheet, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Route } from '../../gen/proto/stationapi_pb' +import { TrainType } from '../../gen/proto/stationapi_pb' import { LED_THEME_BG_COLOR } from '../constants' import { useThemeStore } from '../hooks/useThemeStore' import { APP_THEME } from '../models/Theme' @@ -9,15 +9,15 @@ import { translate } from '../translation' import isTablet from '../utils/isTablet' import FAB from './FAB' import Heading from './Heading' -import { RouteList } from './RouteList' +import { TrainTypeList } from './TrainTypeList' type Props = { + trainTypes: TrainType[] visible: boolean - routes: Route[] loading: boolean error: Error onClose: () => void - onSelect: (route: Route) => void + onSelect: (trainType: TrainType) => void } const styles = StyleSheet.create({ @@ -46,8 +46,8 @@ const styles = StyleSheet.create({ const SAFE_AREA_FALLBACK = 32 export const RouteListModal: React.FC = ({ + trainTypes, visible, - routes, loading, onClose, onSelect, @@ -96,16 +96,18 @@ export const RouteListModal: React.FC = ({ > - {translate('routeSearchTitle')} + {translate('trainTypeSettings')} + + + {loading ? ( + + ) : ( + + )} - {loading ? ( - - ) : ( - - )} diff --git a/src/components/TrainTypeInfoModal.tsx b/src/components/TrainTypeInfoModal.tsx index 9a95a62c8..ef9a7576c 100644 --- a/src/components/TrainTypeInfoModal.tsx +++ b/src/components/TrainTypeInfoModal.tsx @@ -146,7 +146,7 @@ export const TrainTypeInfoModal: React.FC = ({ lineHeight: RFValue(14), }} > - {stopStations.length + {!loading && stopStations.length ? stopStations.map((s) => s.name).join('、') : ''} {loading ? `${translate('loadingAPI')}...` : ''} diff --git a/src/hooks/useStationList.ts b/src/hooks/useStationList.ts index 9c7edfcef..c8eda8795 100644 --- a/src/hooks/useStationList.ts +++ b/src/hooks/useStationList.ts @@ -1,5 +1,7 @@ +import { useEffect } from 'react' import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import useSWR from 'swr' +import useSWRMutation from 'swr/mutation' import { GetStationByLineIdRequest, GetTrainTypesByStationIdRequest, @@ -17,11 +19,12 @@ export const useStationList = () => { const setStationState = useSetRecoilState(stationState) const [{ fromBuilder }, setNavigationState] = useRecoilState(navigationState) const { selectedLine } = useRecoilValue(lineState) + const { stations: stationsFromState } = useRecoilValue(stationState) const { + data: stations, isLoading: isLoadingStations, error: loadingStationsError, - mutate: mutateStations, } = useSWR( [ '/app.trainlcd.grpc/getStationsByLineId', @@ -33,6 +36,13 @@ export const useStationList = () => { return } + if ( + selectedLine?.station?.hasTrainTypes && + stationsFromState.length > 0 + ) { + return + } + const req = new GetStationByLineIdRequest({ lineId, stationId }) const res = await grpcClient.getStationsByLineId(req) @@ -45,63 +55,76 @@ export const useStationList = () => { } ) - const { isLoading: isTrainTypesLoading, error: loadingTrainTypesError } = - useSWR( - [ - '/app.trainlcd.grpc/GetTrainTypesByStationId', - selectedLine?.station?.id, - selectedLine?.station?.hasTrainTypes, - ], - async ([, stationId, shouldFetch]) => { - if (!stationId || !shouldFetch) { - return - } + const { + data: trainTypes = [], + isMutating: isTrainTypesLoading, + error: loadingTrainTypesError, + trigger: fetchTrainTypes, + } = useSWRMutation( + '/app.trainlcd.grpc/GetTrainTypesByStationId', + async (_, { arg: { stationId } }: { arg: { stationId: number } }) => { + if (!stationId) { + return + } - const req = new GetTrainTypesByStationIdRequest({ stationId }) - const res = await grpcClient.getTrainTypesByStationId(req, {}) + const req = new GetTrainTypesByStationIdRequest({ stationId }) + const res = await grpcClient.getTrainTypesByStationId(req, {}) - const trainTypes = res.trainTypes ?? [] - const localType = new TrainType({ - id: 0, - typeId: 0, - groupId: 0, - name: '普通/各駅停車', - nameKatakana: '', - nameRoman: 'Local', - nameChinese: '慢车/每站停车', - nameKorean: '보통/각역정차', - color: '', - lines: [], - direction: TrainDirection.Both, - kind: TrainTypeKind.Default, - }) + const trainTypes = res.trainTypes - // 普通種別が登録済み: 非表示 - // 支線種別が登録されていているが、普通種別が登録されていない: 非表示 - // 特例で普通列車以外の種別で表示を設定されている場合(中央線快速等): 表示 - // 上記以外: 表示 - if ( - !( - findLocalType(trainTypes) || - (findBranchLine(trainTypes) && !findLocalType(trainTypes)) - ) - ) { - setNavigationState((prev) => ({ - ...prev, - fetchedTrainTypes: [localType, ...trainTypes], - })) - return trainTypes - } + const localType = new TrainType({ + id: 0, + typeId: 0, + groupId: 0, + name: '普通/各駅停車', + nameKatakana: '', + nameRoman: 'Local', + nameChinese: '慢车/每站停车', + nameKorean: '보통/각역정차', + color: '', + lines: [], + direction: TrainDirection.Both, + kind: TrainTypeKind.Default, + }) + // 普通種別が登録済み: 非表示 + // 支線種別が登録されていているが、普通種別が登録されていない: 非表示 + // 特例で普通列車以外の種別で表示を設定されている場合(中央線快速等): 表示 + // 上記以外: 表示 + if ( + !( + findLocalType(trainTypes) || + (findBranchLine(trainTypes) && !findLocalType(trainTypes)) + ) + ) { setNavigationState((prev) => ({ ...prev, - fetchedTrainTypes: trainTypes, + fetchedTrainTypes: [localType, ...trainTypes], })) + return [localType, ...trainTypes] } - ) + + setNavigationState((prev) => ({ + ...prev, + fetchedTrainTypes: trainTypes, + })) + + return trainTypes + } + ) + + useEffect(() => { + if (selectedLine?.station) { + fetchTrainTypes({ + stationId: selectedLine.station.id, + }) + } + }, [fetchTrainTypes, selectedLine?.station]) return { - mutateStations, + stations, + fetchTrainTypes, + trainTypes, loading: isLoadingStations || isTrainTypesLoading, error: loadingStationsError || loadingTrainTypesError, } diff --git a/src/hooks/useTrainTypeStations.ts b/src/hooks/useTrainTypeStations.ts new file mode 100644 index 000000000..c9539b0b1 --- /dev/null +++ b/src/hooks/useTrainTypeStations.ts @@ -0,0 +1,23 @@ +import useSWRMutation from 'swr/dist/mutation' +import { GetStationsByLineGroupIdRequest } from '../../gen/proto/stationapi_pb' +import { grpcClient } from '../lib/grpc' + +export const useTrainTypeStations = () => { + const { + data: stations, + isMutating: isLoading, + error, + trigger: fetchStations, + } = useSWRMutation( + '/app.trainlcd.grpc/GetStationsByLineGroupId', + async (_, { arg: { lineGroupId } }: { arg: { lineGroupId: number } }) => { + const req = new GetStationsByLineGroupIdRequest({ + lineGroupId, + }) + const res = await grpcClient.getStationsByLineGroupId(req) + return res.stations ?? [] + } + ) + + return { stations, isLoading, error, fetchStations } +} diff --git a/src/screens/RouteSearchScreen.tsx b/src/screens/RouteSearchScreen.tsx index 31504d0d0..07b725736 100644 --- a/src/screens/RouteSearchScreen.tsx +++ b/src/screens/RouteSearchScreen.tsx @@ -21,11 +21,8 @@ import useSWRMutation from 'swr/mutation' import { GetRouteRequest, GetStationsByNameRequest, - Route, Station, - TrainDirection, TrainType, - TrainTypeKind, } from '../../gen/proto/stationapi_pb' import FAB from '../components/FAB' import Heading from '../components/Heading' @@ -33,7 +30,9 @@ import { RouteListModal } from '../components/RouteListModal' import { StationList } from '../components/StationList' import { FONTS } from '../constants' import { useCurrentStation } from '../hooks/useCurrentStation' +import { useStationList } from '../hooks/useStationList' import { useThemeStore } from '../hooks/useThemeStore' +import { useTrainTypeStations } from '../hooks/useTrainTypeStations' import { grpcClient } from '../lib/grpc' import { APP_THEME } from '../models/Theme' import lineState from '../store/atoms/line' @@ -41,7 +40,6 @@ import navigationState from '../store/atoms/navigation' import stationState from '../store/atoms/station' import { translate } from '../translation' import { groupStations } from '../utils/groupStations' -import getIsPass from '../utils/isPass' const styles = StyleSheet.create({ root: { @@ -85,6 +83,9 @@ const RouteSearchScreen = () => { const setNavigationState = useSetRecoilState(navigationState) const currentStation = useCurrentStation() + const { fetchStations: fetchTrainTypeFromTrainTypeId } = + useTrainTypeStations() + const { trainTypes, fetchTrainTypes } = useStationList() const { data: byNameData, @@ -113,14 +114,10 @@ const RouteSearchScreen = () => { } ) - const { - data: routesData, - isLoading: isRoutesLoading, - error: fetchRoutesError, - } = useSWR( + const { isLoading: isRoutesLoading, error: fetchRoutesError } = useSWR( ['/app.trainlcd.grpc/getRoutes', selectedStation?.groupId], async ([, toStationGroupId]) => { - if (!currentStation) { + if (!currentStation || !toStationGroupId) { return [] } @@ -134,21 +131,6 @@ const RouteSearchScreen = () => { } ) - const withoutPassStationRoutes = useMemo( - () => - routesData?.filter((route) => - // NOTE: 両方の駅どちらも停車する種別を探す - route.stops - .filter( - (stop) => - stop.groupId === currentStation?.groupId || - stop.groupId === selectedStation?.groupId - ) - .every((stop) => !getIsPass(stop, true)) - ) ?? [], - [currentStation?.groupId, routesData, selectedStation?.groupId] - ) - const onPressBack = useCallback(() => { if (navigation.canGoBack()) { navigation.goBack() @@ -177,15 +159,24 @@ const RouteSearchScreen = () => { ) const handleStationPress = useCallback( - (stationFromSearch: Station) => { - const station = foundStations.find((s) => s.id === stationFromSearch.id) - if (!station) { + async (stationFromSearch: Station) => { + setLineState((prev) => ({ + ...prev, + selectedLine: stationFromSearch.line ?? null, + })) + setSelectedStation(stationFromSearch) + + if (stationFromSearch.hasTrainTypes) { + await fetchTrainTypes({ + stationId: stationFromSearch.id, + }) + setIsRouteListModalVisible(true) return } - setSelectedStation(station) - setIsRouteListModalVisible(true) + + navigation.navigate('SelectBound') }, - [foundStations] + [fetchTrainTypes, navigation, setLineState] ) const onKeyPress = useCallback( @@ -205,83 +196,26 @@ const RouteSearchScreen = () => { ) const handleSelect = useCallback( - (route: Route) => { - const matchedStation = route.stops.find( - (s) => s.groupId === currentStation?.groupId - ) - const line = matchedStation?.line - const matchedStationIndex = route.stops.findIndex( - (s) => s.groupId === matchedStation?.groupId - ) - const boundStationIndex = route.stops.findIndex( - (s) => s?.groupId === selectedStation?.groupId - ) - const direction = - matchedStationIndex < boundStationIndex ? 'INBOUND' : 'OUTBOUND' - - const stops = - direction === 'INBOUND' ? route.stops : route.stops.slice().reverse() - const currentStationIndex = stops.findIndex( - (stop) => stop.groupId === currentStation?.groupId - ) - const stopsAfterCurrentStation = stops.slice(currentStationIndex) - - if (line) { - setStationState((prev) => ({ - ...prev, - stations: stopsAfterCurrentStation, - })) - - const trainTypes = - withoutPassStationRoutes - ?.map((route) => - route.stops.find( - (stop) => stop.groupId === currentStation?.groupId - ) - ) - .map((stop) => { - if (stop?.trainType) { - return stop?.trainType - } - - return new TrainType({ - id: 0, - typeId: 0, - groupId: 0, - name: '普通/各駅停車', - nameKatakana: '', - nameRoman: 'Local', - nameChinese: '慢车/每站停车', - nameKorean: '보통/각역정차', - color: '', - lines: stop?.lines, - direction: TrainDirection.Both, - kind: TrainTypeKind.Default, - }) - }) ?? [] - - setNavigationState((prev) => ({ - ...prev, - trainType: matchedStation.trainType ?? null, - fetchedTrainTypes: trainTypes, - leftStations: [], - fromBuilder: true, - })) - setLineState((prev) => ({ - ...prev, - selectedLine: line, - })) + async (trainType: TrainType) => { + if (!trainType.id) { + setNavigationState((prev) => ({ ...prev, trainType: null })) navigation.navigate('SelectBound') + return } + + const stations = await fetchTrainTypeFromTrainTypeId({ + lineGroupId: trainType.groupId, + }) + + setNavigationState((prev) => ({ ...prev, trainType })) + setStationState((prev) => ({ ...prev, stations })) + navigation.navigate('SelectBound') }, [ - currentStation?.groupId, + fetchTrainTypeFromTrainTypeId, navigation, - selectedStation?.groupId, - setLineState, setNavigationState, setStationState, - withoutPassStationRoutes, ] ) @@ -328,8 +262,8 @@ const RouteSearchScreen = () => { setIsRouteListModalVisible(false)} diff --git a/src/screens/SelectBound.tsx b/src/screens/SelectBound.tsx index 8cf1696fb..fb3bf39b2 100644 --- a/src/screens/SelectBound.tsx +++ b/src/screens/SelectBound.tsx @@ -78,7 +78,7 @@ const SelectBoundScreen: React.FC = () => { useRecoilState(navigationState) const [{ selectedLine }, setLineState] = useRecoilState(lineState) - const { loading, error, mutateStations } = useStationList() + const { loading, error, fetchStations } = useStationList() const { isLoopLine, isMeijoLine } = useLoopLine() const { bounds: [inboundStations, outboundStations], @@ -324,7 +324,7 @@ const SelectBoundScreen: React.FC = () => { showStatus title={translate('errorTitle')} text={translate('apiErrorText')} - onRetryPress={mutateStations} + onRetryPress={fetchStations} isFetching={loading} /> )