Skip to content

Commit 4b7c28c

Browse files
TinyKittenclaude
andcommitted
プリセット名入力モーダルの追加とwantedDestinationIdのサポート
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fdd1bf8 commit 4b7c28c

File tree

5 files changed

+160
-5
lines changed

5 files changed

+160
-5
lines changed

assets/translations/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,5 +277,8 @@
277277
"settingsWalkthroughDescription3": "Configure auto announcement settings. You can receive voice guidance when arriving at stations.",
278278
"settingsWalkthroughTitle4": "Display Languages",
279279
"settingsWalkthroughDescription4": "Set the languages to display on screen. You can choose from Japanese, English, Chinese, and Korean.",
280-
"requireJapaneseOrEnglish": "Either Japanese or English must be set."
280+
"requireJapaneseOrEnglish": "Either Japanese or English must be set.",
281+
"presetNameInputTitle": "Enter preset name",
282+
"presetNamePlaceholder": "Preset name",
283+
"save": "Save"
281284
}

assets/translations/ja.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,5 +278,8 @@
278278
"settingsWalkthroughDescription3": "自動アナウンス機能の設定ができます。駅到着時に音声で案内を受け取ることができます。",
279279
"settingsWalkthroughTitle4": "表示言語",
280280
"settingsWalkthroughDescription4": "画面に表示する言語を設定できます。日本語、英語、中国語、韓国語から選択可能です。",
281-
"requireJapaneseOrEnglish": "日本語または英語のいずれかを設定する必要があります"
281+
"requireJapaneseOrEnglish": "日本語または英語のいずれかを設定する必要があります",
282+
"presetNameInputTitle": "プリセット名を入力",
283+
"presetNamePlaceholder": "プリセット名",
284+
"save": "保存"
282285
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { useAtomValue } from 'jotai';
2+
import type React from 'react';
3+
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import {
5+
Keyboard,
6+
Platform,
7+
Pressable,
8+
StyleSheet,
9+
TextInput,
10+
type TextInput as TextInputType,
11+
View,
12+
} from 'react-native';
13+
import { FONTS, LED_THEME_BG_COLOR } from '~/constants';
14+
import { isLEDThemeAtom } from '~/store/atoms/theme';
15+
import { translate } from '~/translation';
16+
import { RFValue } from '~/utils/rfValue';
17+
import Button from './Button';
18+
import { CustomModal } from './CustomModal';
19+
import { Heading } from './Heading';
20+
21+
type Props = {
22+
visible: boolean;
23+
onClose: () => void;
24+
onSubmit: (name: string) => void;
25+
defaultName: string;
26+
};
27+
28+
const styles = StyleSheet.create({
29+
contentView: {
30+
width: '100%',
31+
paddingVertical: 24,
32+
paddingHorizontal: 24,
33+
},
34+
textInput: {
35+
borderWidth: 1,
36+
borderColor: '#aaa',
37+
paddingHorizontal: 16,
38+
paddingVertical: 12,
39+
width: '100%',
40+
fontSize: RFValue(14),
41+
marginTop: 12,
42+
borderRadius: 8,
43+
},
44+
buttonContainer: {
45+
alignItems: 'center',
46+
flexDirection: 'row',
47+
justifyContent: 'center',
48+
marginTop: 24,
49+
gap: 16,
50+
},
51+
saveButton: {
52+
width: 120,
53+
},
54+
});
55+
56+
export const SavePresetNameModal: React.FC<Props> = ({
57+
visible,
58+
onClose,
59+
onSubmit,
60+
defaultName,
61+
}) => {
62+
const isLEDTheme = useAtomValue(isLEDThemeAtom);
63+
const textInputRef = useRef<TextInputType>(null);
64+
const textRef = useRef(defaultName);
65+
const [isEmpty, setIsEmpty] = useState(false);
66+
67+
useEffect(() => {
68+
if (visible) {
69+
textRef.current = defaultName;
70+
setIsEmpty(!defaultName.trim());
71+
}
72+
}, [visible, defaultName]);
73+
74+
const handleChangeText = useCallback((text: string) => {
75+
textRef.current = text;
76+
setIsEmpty(!text.trim());
77+
}, []);
78+
79+
const handleSubmit = useCallback(() => {
80+
const name = textRef.current.trim();
81+
if (name) {
82+
onSubmit(name);
83+
}
84+
}, [onSubmit]);
85+
86+
return (
87+
<CustomModal
88+
visible={visible}
89+
onClose={onClose}
90+
backdropStyle={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
91+
contentContainerStyle={[
92+
styles.contentView,
93+
{
94+
backgroundColor: isLEDTheme ? LED_THEME_BG_COLOR : '#fff',
95+
borderRadius: isLEDTheme ? 0 : 8,
96+
},
97+
]}
98+
avoidKeyboard
99+
>
100+
<Pressable onPress={Keyboard.dismiss}>
101+
<Heading>{translate('presetNameInputTitle')}</Heading>
102+
103+
<TextInput
104+
ref={textInputRef}
105+
autoFocus={Platform.OS === 'ios'}
106+
defaultValue={defaultName}
107+
onChangeText={handleChangeText}
108+
style={[
109+
styles.textInput,
110+
{
111+
color: isLEDTheme ? '#fff' : '#000',
112+
fontFamily: isLEDTheme ? FONTS.JFDotJiskan24h : undefined,
113+
},
114+
]}
115+
placeholder={translate('presetNamePlaceholder')}
116+
returnKeyType="done"
117+
onSubmitEditing={handleSubmit}
118+
/>
119+
120+
<View style={styles.buttonContainer}>
121+
<Button onPress={onClose} outline>
122+
{translate('cancel')}
123+
</Button>
124+
<Button
125+
style={styles.saveButton}
126+
disabled={isEmpty}
127+
onPress={handleSubmit}
128+
>
129+
{translate('save')}
130+
</Button>
131+
</View>
132+
</Pressable>
133+
</CustomModal>
134+
);
135+
};

src/hooks/useSavedRoutes.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface SavedRouteRow {
1111
name: string;
1212
lineId: number;
1313
trainTypeId: number | null;
14+
wantedDestinationId: number | null;
1415
hasTrainType: number; // SQLiteではBOOLEANが数値として保存される
1516
createdAt: string; // SQLiteでは日時が文字列として保存される
1617
}
@@ -31,6 +32,7 @@ const convertRowToSavedRoute = (row: SavedRouteRow): SavedRoute | null => {
3132
name: row.name,
3233
lineId: row.lineId,
3334
trainTypeId: row.trainTypeId,
35+
wantedDestinationId: row.wantedDestinationId ?? null,
3436
hasTrainType: true,
3537
createdAt: new Date(row.createdAt),
3638
};
@@ -40,6 +42,7 @@ const convertRowToSavedRoute = (row: SavedRouteRow): SavedRoute | null => {
4042
name: row.name,
4143
lineId: row.lineId,
4244
trainTypeId: null,
45+
wantedDestinationId: row.wantedDestinationId ?? null,
4346
hasTrainType: false,
4447
createdAt: new Date(row.createdAt),
4548
};
@@ -74,6 +77,14 @@ export const useSavedRoutes = () => {
7477
await db.execAsync(
7578
'CREATE INDEX IF NOT EXISTS idx_saved_routes_ttype_dest_has ON saved_routes(trainTypeId, hasTrainType, createdAt DESC);'
7679
);
80+
// wantedDestinationId カラムを既存テーブルに追加(存在しない場合のみ)
81+
try {
82+
await db.execAsync(
83+
'ALTER TABLE saved_routes ADD COLUMN wantedDestinationId INTEGER;'
84+
);
85+
} catch {
86+
// カラムが既に存在する場合は無視
87+
}
7788
setNavigationAtom((prev) => ({ ...prev, presetsFetched: true }));
7889
};
7990
initDb();
@@ -130,14 +141,15 @@ export const useSavedRoutes = () => {
130141
} as SavedRoute;
131142

132143
await db.runAsync(
133-
`INSERT INTO saved_routes
134-
(id, name, lineId, trainTypeId, hasTrainType, createdAt)
135-
VALUES (?, ?, ?, ?, ?, ?)`,
144+
`INSERT INTO saved_routes
145+
(id, name, lineId, trainTypeId, wantedDestinationId, hasTrainType, createdAt)
146+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
136147
[
137148
newRoute.id,
138149
newRoute.name,
139150
newRoute.lineId,
140151
newRoute.trainTypeId ?? null,
152+
newRoute.wantedDestinationId ?? null,
141153
newRoute.hasTrainType ? 1 : 0,
142154
newRoute.createdAt.toISOString(),
143155
]

src/models/SavedRoute.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const SavedRouteWithTrainTypeSchema = z.object({
55
hasTrainType: z.literal(true),
66
lineId: z.number().int().nonnegative(),
77
trainTypeId: z.number().int().nonnegative(),
8+
wantedDestinationId: z.number().int().nonnegative().nullable(),
89
name: z.string().min(1).max(100),
910
createdAt: z.date(),
1011
});
@@ -18,6 +19,7 @@ export const SavedRouteWithoutTrainTypeSchema = z.object({
1819
hasTrainType: z.literal(false),
1920
lineId: z.number().int().nonnegative(),
2021
trainTypeId: z.null(),
22+
wantedDestinationId: z.number().int().nonnegative().nullable(),
2123
name: z.string().min(1).max(100),
2224
createdAt: z.date(),
2325
});

0 commit comments

Comments
 (0)