create-pixi-vn 2.0.14 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template-react-vite-muijoy/package-lock.json +12 -8
- package/template-react-vite-muijoy/package.json +1 -1
- package/template-react-vite-muijoy/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy/src/locales/en.json +2 -0
- package/template-react-vite-muijoy/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-ink/package-lock.json +12 -8
- package/template-react-vite-muijoy-ink/package.json +1 -1
- package/template-react-vite-muijoy-ink/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-ink/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-ink/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-ink/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-ink/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-ink/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-ink/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-ink/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-ink/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-ink/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-ink/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-ink/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-ink/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-ink/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-ink/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-ink-tauri/package-lock.json +12 -8
- package/template-react-vite-muijoy-ink-tauri/package.json +1 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-ink-tauri/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-ink-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-ink-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-ink-tauri/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-ink-tauri/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-ink-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-ink-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-ink-tauri/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-ink-tauri/src/routes/__root.tsx +3 -4
- package/template-react-vite-muijoy-tauri/package-lock.json +12 -8
- package/template-react-vite-muijoy-tauri/package.json +1 -1
- package/template-react-vite-muijoy-tauri/src/components/loading.tsx +11 -1
- package/template-react-vite-muijoy-tauri/src/components/menus/choice-menus.tsx +4 -0
- package/template-react-vite-muijoy-tauri/src/components/menus/main-menu.tsx +1 -6
- package/template-react-vite-muijoy-tauri/src/components/menus/settings/index.tsx +3 -1
- package/template-react-vite-muijoy-tauri/src/components/modals/input-request-dialogues.tsx +1 -1
- package/template-react-vite-muijoy-tauri/src/components/modals/quick-actions-wheel.tsx +3 -1
- package/template-react-vite-muijoy-tauri/src/components/providers/alert-dialog-provider.tsx +3 -0
- package/template-react-vite-muijoy-tauri/src/components/screens/narration/narration-cards.tsx +40 -19
- package/template-react-vite-muijoy-tauri/src/lib/hooks/hotkeys-hooks.ts +111 -10
- package/template-react-vite-muijoy-tauri/src/lib/hooks/narration-hooks.ts +7 -1
- package/template-react-vite-muijoy-tauri/src/lib/query/narration-query.ts +32 -2
- package/template-react-vite-muijoy-tauri/src/lib/stores/alert-dialog-store.ts +13 -0
- package/template-react-vite-muijoy-tauri/src/lib/stores/text-display-settings-store.ts +20 -1
- package/template-react-vite-muijoy-tauri/src/locales/en.json +2 -0
- package/template-react-vite-muijoy-tauri/src/routes/__root.tsx +3 -4
|
@@ -2,22 +2,47 @@ import { useAlertDialog } from "@/components/providers/alert-dialog-provider";
|
|
|
2
2
|
import { useNarrationFunctions } from "@/lib/hooks/narration-hooks";
|
|
3
3
|
import { useSetSearchParamState } from "@/lib/hooks/navigation-hooks";
|
|
4
4
|
import { useGameProps } from "@/lib/hooks/props-hooks";
|
|
5
|
-
import { useQueryInputValue } from "@/lib/query/narration-query";
|
|
5
|
+
import { useQueryCanGoNext, useQueryInputValue } from "@/lib/query/narration-query";
|
|
6
6
|
import {
|
|
7
7
|
LAST_SAVE_USE_QUERY_KEY,
|
|
8
8
|
SAVES_USE_QUERY_KEY,
|
|
9
9
|
useQueryLastSave,
|
|
10
10
|
} from "@/lib/query/save-query";
|
|
11
|
+
import { AlertDialogState } from "@/lib/stores/alert-dialog-store";
|
|
11
12
|
import { QuickActionsWheelState } from "@/lib/stores/quick-actions-wheel-store";
|
|
13
|
+
import { SearchParams } from "@/lib/stores/search-param-store";
|
|
12
14
|
import { SkipSettings } from "@/lib/stores/skip-settings-store";
|
|
15
|
+
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
13
16
|
import { loadSave, saveGameToIndexDB } from "@/lib/utils/save-utility";
|
|
17
|
+
import { narration } from "@drincs/pixi-vn";
|
|
14
18
|
import { useHotkeys } from "@tanstack/react-hotkeys";
|
|
15
19
|
import { useQueryClient } from "@tanstack/react-query";
|
|
16
20
|
import { useLocation } from "@tanstack/react-router";
|
|
17
|
-
import {
|
|
21
|
+
import { useSelector } from "@tanstack/react-store";
|
|
22
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
18
23
|
import { useTranslation } from "react-i18next";
|
|
19
24
|
import { toast } from "sonner";
|
|
20
25
|
|
|
26
|
+
function useMenuDialogState() {
|
|
27
|
+
const searchParams = useSelector(SearchParams.store, (state) => state);
|
|
28
|
+
const alertDialogCount = useSelector(AlertDialogState.store, (state) => state.count);
|
|
29
|
+
const { data: { isRequired } = {} } = useQueryInputValue<string | number>();
|
|
30
|
+
|
|
31
|
+
/** true when the settings menu (or any sub-page) is open */
|
|
32
|
+
const isSettingsOpen = useMemo(
|
|
33
|
+
() => Object.values(searchParams).some((v) => v !== undefined),
|
|
34
|
+
[searchParams],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** true when an alert confirmation dialog or the input-request dialog is blocking interaction */
|
|
38
|
+
const isAlertOrInputOpen = alertDialogCount > 0 || !!isRequired;
|
|
39
|
+
|
|
40
|
+
/** true when any menu or dialog that should disable hotkeys is open */
|
|
41
|
+
const isAnyMenuOrDialogOpen = isSettingsOpen || isAlertOrInputOpen;
|
|
42
|
+
|
|
43
|
+
return { isAnyMenuOrDialogOpen, isSettingsOpen, isAlertOrInputOpen };
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
/**
|
|
22
47
|
* useSaveHotkeys
|
|
23
48
|
*
|
|
@@ -40,6 +65,7 @@ export function useSaveHotkeys(): null {
|
|
|
40
65
|
const { data: lastSave = null } = useQueryLastSave();
|
|
41
66
|
const { openAlertDialog } = useAlertDialog();
|
|
42
67
|
const gameProps = useGameProps();
|
|
68
|
+
const { isSettingsOpen } = useMenuDialogState();
|
|
43
69
|
|
|
44
70
|
const quickSave = useCallback(() => {
|
|
45
71
|
if (location.pathname === "/") {
|
|
@@ -87,6 +113,7 @@ export function useSaveHotkeys(): null {
|
|
|
87
113
|
hotkey: "F5",
|
|
88
114
|
callback: quickSave,
|
|
89
115
|
options: {
|
|
116
|
+
enabled: !isSettingsOpen,
|
|
90
117
|
meta: {
|
|
91
118
|
name: t("quick_save"),
|
|
92
119
|
description: t("quick_save_hotkey_description"),
|
|
@@ -97,6 +124,7 @@ export function useSaveHotkeys(): null {
|
|
|
97
124
|
hotkey: "Control+S",
|
|
98
125
|
callback: quickSave,
|
|
99
126
|
options: {
|
|
127
|
+
enabled: !isSettingsOpen,
|
|
100
128
|
meta: {
|
|
101
129
|
name: t("quick_save"),
|
|
102
130
|
description: t("quick_save_hotkey_alternative_description"),
|
|
@@ -107,6 +135,7 @@ export function useSaveHotkeys(): null {
|
|
|
107
135
|
hotkey: "F9",
|
|
108
136
|
callback: quickLoad,
|
|
109
137
|
options: {
|
|
138
|
+
enabled: !isSettingsOpen,
|
|
110
139
|
meta: {
|
|
111
140
|
name: t("load_last_save"),
|
|
112
141
|
description: t("quick_load_hotkey_description"),
|
|
@@ -117,6 +146,7 @@ export function useSaveHotkeys(): null {
|
|
|
117
146
|
hotkey: "Control+L",
|
|
118
147
|
callback: quickLoad,
|
|
119
148
|
options: {
|
|
149
|
+
enabled: !isSettingsOpen,
|
|
120
150
|
meta: {
|
|
121
151
|
name: t("load_last_save"),
|
|
122
152
|
description: t("quick_load_hotkey_alternative_description"),
|
|
@@ -132,6 +162,7 @@ export function useSettingsHotkeys(): null {
|
|
|
132
162
|
const { t } = useTranslation(["ui"]);
|
|
133
163
|
const setSettingsOpen = useSetSearchParamState<boolean>("settings");
|
|
134
164
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
165
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
135
166
|
|
|
136
167
|
const openControlsPage = useCallback(() => {
|
|
137
168
|
setSettingsOpen(true);
|
|
@@ -150,9 +181,10 @@ export function useSettingsHotkeys(): null {
|
|
|
150
181
|
},
|
|
151
182
|
},
|
|
152
183
|
{
|
|
153
|
-
hotkey: "
|
|
184
|
+
hotkey: "K",
|
|
154
185
|
callback: openControlsPage,
|
|
155
186
|
options: {
|
|
187
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
156
188
|
meta: {
|
|
157
189
|
name: t("hotkeys_menu"),
|
|
158
190
|
description: t("hotkeys_menu_shortcut_description"),
|
|
@@ -169,6 +201,7 @@ export function useGameHotkeys(): null {
|
|
|
169
201
|
const setSettingsTab = useSetSearchParamState<string>("settings_tab");
|
|
170
202
|
const setHistory = useSetSearchParamState<boolean>("history");
|
|
171
203
|
const { t } = useTranslation(["ui"]);
|
|
204
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
172
205
|
|
|
173
206
|
const openHistoryPage = useCallback(() => {
|
|
174
207
|
setHistory(undefined);
|
|
@@ -187,9 +220,10 @@ export function useGameHotkeys(): null {
|
|
|
187
220
|
|
|
188
221
|
useHotkeys([
|
|
189
222
|
{
|
|
190
|
-
hotkey: "
|
|
223
|
+
hotkey: "H",
|
|
191
224
|
callback: openHistoryPage,
|
|
192
225
|
options: {
|
|
226
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
193
227
|
meta: {
|
|
194
228
|
name: t("history"),
|
|
195
229
|
description: t("history_hotkey_description"),
|
|
@@ -200,6 +234,7 @@ export function useGameHotkeys(): null {
|
|
|
200
234
|
hotkey: "Tab",
|
|
201
235
|
callback: toggleQuickActionsWheel,
|
|
202
236
|
options: {
|
|
237
|
+
enabled: !isAnyMenuOrDialogOpen,
|
|
203
238
|
meta: {
|
|
204
239
|
name: t("quick_actions"),
|
|
205
240
|
description: t("quick_actions_open_description"),
|
|
@@ -211,38 +246,104 @@ export function useGameHotkeys(): null {
|
|
|
211
246
|
return null;
|
|
212
247
|
}
|
|
213
248
|
|
|
249
|
+
export function useChoiceMenuHotkeys(menuLength: number) {
|
|
250
|
+
const { t } = useTranslation(["ui"]);
|
|
251
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
252
|
+
|
|
253
|
+
function getMenuItems(): HTMLButtonElement[] {
|
|
254
|
+
if (!menuRef.current) return [];
|
|
255
|
+
return Array.from(
|
|
256
|
+
menuRef.current.querySelectorAll<HTMLButtonElement>(
|
|
257
|
+
"button[role='menuitem']:not(:disabled)",
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function focusMenuItem(direction: "up" | "down") {
|
|
263
|
+
const items = getMenuItems();
|
|
264
|
+
if (!items.length) return;
|
|
265
|
+
const active = document.activeElement as HTMLElement;
|
|
266
|
+
const currentIndex = items.indexOf(active as HTMLButtonElement);
|
|
267
|
+
let next: number;
|
|
268
|
+
if (direction === "down") {
|
|
269
|
+
next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
270
|
+
} else {
|
|
271
|
+
next = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
272
|
+
}
|
|
273
|
+
items[next].focus();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
useHotkeys([
|
|
277
|
+
{
|
|
278
|
+
hotkey: "ArrowDown",
|
|
279
|
+
callback: () => focusMenuItem("down"),
|
|
280
|
+
options: {
|
|
281
|
+
enabled: menuLength > 0,
|
|
282
|
+
preventDefault: true,
|
|
283
|
+
meta: {
|
|
284
|
+
name: t("choice_navigation"),
|
|
285
|
+
description: t("choice_navigation_description"),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
hotkey: "ArrowUp",
|
|
291
|
+
callback: () => focusMenuItem("up"),
|
|
292
|
+
options: {
|
|
293
|
+
enabled: menuLength > 0,
|
|
294
|
+
preventDefault: true,
|
|
295
|
+
meta: {
|
|
296
|
+
name: t("choice_navigation"),
|
|
297
|
+
description: t("choice_navigation_description"),
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
return { menuRef };
|
|
304
|
+
}
|
|
305
|
+
|
|
214
306
|
export function useNarrationHotkeys(): null {
|
|
215
307
|
const { t } = useTranslation(["ui"]);
|
|
216
308
|
const { goNext } = useNarrationFunctions();
|
|
217
|
-
const
|
|
309
|
+
const typewriterInProgress = useSelector(
|
|
310
|
+
TextDisplaySettings.store,
|
|
311
|
+
(state) => state.inProgress,
|
|
312
|
+
);
|
|
313
|
+
const { isAnyMenuOrDialogOpen } = useMenuDialogState();
|
|
314
|
+
const { data: canGoNext } = useQueryCanGoNext();
|
|
218
315
|
|
|
219
316
|
const onSkipKeyDown = useCallback(() => SkipSettings.setEnabled(true), []);
|
|
220
317
|
const onSkipKeyUp = useCallback(() => {
|
|
221
318
|
SkipSettings.setEnabled(false);
|
|
319
|
+
if (typewriterInProgress && !narration.dialogGlue) {
|
|
320
|
+
TextDisplaySettings.complete();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
222
323
|
goNext();
|
|
223
|
-
}, [goNext]);
|
|
324
|
+
}, [goNext, typewriterInProgress]);
|
|
224
325
|
|
|
225
326
|
useHotkeys([
|
|
226
327
|
{
|
|
227
328
|
hotkey: "Enter",
|
|
228
329
|
callback: onSkipKeyDown,
|
|
229
330
|
options: {
|
|
331
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
230
332
|
meta: {
|
|
231
333
|
name: t("skip"),
|
|
232
334
|
description: t("skip_hold_description"),
|
|
233
335
|
},
|
|
234
|
-
enabled: !isRequired,
|
|
235
336
|
},
|
|
236
337
|
},
|
|
237
338
|
{
|
|
238
339
|
hotkey: "Space",
|
|
239
340
|
callback: onSkipKeyDown,
|
|
240
341
|
options: {
|
|
342
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
241
343
|
meta: {
|
|
242
344
|
name: t("skip"),
|
|
243
345
|
description: t("skip_hold_space_description"),
|
|
244
346
|
},
|
|
245
|
-
enabled: !isRequired,
|
|
246
347
|
},
|
|
247
348
|
},
|
|
248
349
|
{
|
|
@@ -251,11 +352,11 @@ export function useNarrationHotkeys(): null {
|
|
|
251
352
|
options: {
|
|
252
353
|
eventType: "keyup",
|
|
253
354
|
conflictBehavior: "allow",
|
|
355
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
254
356
|
meta: {
|
|
255
357
|
name: t("next"),
|
|
256
358
|
description: t("skip_release_description"),
|
|
257
359
|
},
|
|
258
|
-
enabled: !isRequired,
|
|
259
360
|
},
|
|
260
361
|
},
|
|
261
362
|
{
|
|
@@ -264,11 +365,11 @@ export function useNarrationHotkeys(): null {
|
|
|
264
365
|
options: {
|
|
265
366
|
eventType: "keyup",
|
|
266
367
|
conflictBehavior: "allow",
|
|
368
|
+
enabled: !isAnyMenuOrDialogOpen && canGoNext,
|
|
267
369
|
meta: {
|
|
268
370
|
name: t("next"),
|
|
269
371
|
description: t("skip_release_space_description"),
|
|
270
372
|
},
|
|
271
|
-
enabled: !isRequired,
|
|
272
373
|
},
|
|
273
374
|
},
|
|
274
375
|
]);
|
|
@@ -22,6 +22,7 @@ export function useNarrationFunctions() {
|
|
|
22
22
|
|
|
23
23
|
const goNext = useCallback(async () => {
|
|
24
24
|
if (hasOpenMenu) return;
|
|
25
|
+
TextDisplaySettings.resetForNext();
|
|
25
26
|
GameStatus.setLoading(true);
|
|
26
27
|
try {
|
|
27
28
|
if (!narration.canContinue) {
|
|
@@ -94,6 +95,7 @@ const isDragGesture = (dx: number, dy: number) =>
|
|
|
94
95
|
export function useNarrationPointerHandlers() {
|
|
95
96
|
const { goNext } = useNarrationFunctions();
|
|
96
97
|
const skipEnabled = useSelector(SkipSettings.store, (state) => state.enabled);
|
|
98
|
+
const typewriterInProgress = useSelector(TextDisplaySettings.store, (state) => state.inProgress);
|
|
97
99
|
const pointerDownPos = useRef<{ x: number; y: number } | null>(null);
|
|
98
100
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
99
101
|
const longPressTriggered = useRef(false);
|
|
@@ -166,9 +168,13 @@ export function useNarrationPointerHandlers() {
|
|
|
166
168
|
if (skipEnabled) {
|
|
167
169
|
SkipSettings.setEnabled(false);
|
|
168
170
|
}
|
|
171
|
+
if (typewriterInProgress && !narration.dialogGlue) {
|
|
172
|
+
TextDisplaySettings.complete();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
169
175
|
goNext();
|
|
170
176
|
},
|
|
171
|
-
[clearLongPressTimer, goNext, skipEnabled],
|
|
177
|
+
[clearLongPressTimer, goNext, skipEnabled, typewriterInProgress],
|
|
172
178
|
);
|
|
173
179
|
|
|
174
180
|
const handlePointerMove = useCallback(
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { INTERFACE_DATA_USE_QUERY_KEY } from "@/constants";
|
|
2
|
+
import { TextDisplaySettings } from "@/lib/stores/text-display-settings-store";
|
|
2
3
|
import { type CharacterInterface, narration, stepHistory } from "@drincs/pixi-vn";
|
|
3
4
|
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { useSelector } from "@tanstack/react-store";
|
|
6
|
+
import { useCallback } from "react";
|
|
4
7
|
import { useTranslation } from "react-i18next";
|
|
5
8
|
|
|
6
9
|
const CAN_GO_BACK_USE_QUERY_KEY = "can_go_back_use_query_key";
|
|
@@ -24,7 +27,6 @@ export function useQueryChoiceMenuOptions() {
|
|
|
24
27
|
? t(option.text)
|
|
25
28
|
: option.text.map((text) => t(text)).join(" "),
|
|
26
29
|
})) || [],
|
|
27
|
-
placeholderData: keepPreviousData,
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -45,13 +47,30 @@ type DialogueModel = {
|
|
|
45
47
|
animatedText?: string;
|
|
46
48
|
text?: string;
|
|
47
49
|
character?: CharacterInterface;
|
|
50
|
+
lastText?: string;
|
|
48
51
|
};
|
|
49
52
|
const DIALOGUE_USE_QUERY_KEY = "dialogue_use_query_key";
|
|
50
53
|
export function useQueryDialogue() {
|
|
51
54
|
const { t } = useTranslation(["narration"]);
|
|
52
55
|
const queryClient = useQueryClient();
|
|
56
|
+
const forceComplete = useSelector(TextDisplaySettings.store, (state) => state.forceComplete);
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
const finalizeDialogue = useCallback(() => {
|
|
59
|
+
queryClient.setQueryData<DialogueModel>(
|
|
60
|
+
[INTERFACE_DATA_USE_QUERY_KEY, DIALOGUE_USE_QUERY_KEY],
|
|
61
|
+
(prev) => {
|
|
62
|
+
if (!prev) return prev;
|
|
63
|
+
const fullText = (prev.text || "") + (prev.animatedText || "");
|
|
64
|
+
return {
|
|
65
|
+
...prev,
|
|
66
|
+
text: fullText || undefined,
|
|
67
|
+
animatedText: undefined,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}, [queryClient]);
|
|
72
|
+
|
|
73
|
+
const query = useQuery<DialogueModel>({
|
|
55
74
|
queryKey: [INTERFACE_DATA_USE_QUERY_KEY, DIALOGUE_USE_QUERY_KEY],
|
|
56
75
|
queryFn: async ({ queryKey }) => {
|
|
57
76
|
const dialogue = narration.dialogue;
|
|
@@ -78,6 +97,7 @@ export function useQueryDialogue() {
|
|
|
78
97
|
}
|
|
79
98
|
return {
|
|
80
99
|
animatedText: newText,
|
|
100
|
+
lastText: newText,
|
|
81
101
|
text: oldText,
|
|
82
102
|
character: character,
|
|
83
103
|
};
|
|
@@ -85,11 +105,21 @@ export function useQueryDialogue() {
|
|
|
85
105
|
|
|
86
106
|
return {
|
|
87
107
|
animatedText: text,
|
|
108
|
+
lastText: text,
|
|
88
109
|
character: character,
|
|
89
110
|
};
|
|
90
111
|
},
|
|
91
112
|
placeholderData: keepPreviousData,
|
|
113
|
+
select: forceComplete
|
|
114
|
+
? (data) => ({
|
|
115
|
+
...data,
|
|
116
|
+
text: (data.text || "") + (data.animatedText || "") || undefined,
|
|
117
|
+
animatedText: undefined,
|
|
118
|
+
})
|
|
119
|
+
: undefined,
|
|
92
120
|
});
|
|
121
|
+
|
|
122
|
+
return { ...query, finalizeDialogue };
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
const CAN_GO_NEXT_USE_QUERY_KEY = "can_go_next_use_query_key";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Store } from "@tanstack/store";
|
|
2
|
+
|
|
3
|
+
export namespace AlertDialogState {
|
|
4
|
+
export const store = new Store<{ count: number }>({ count: 0 });
|
|
5
|
+
|
|
6
|
+
export function increment() {
|
|
7
|
+
store.setState((state) => ({ count: state.count + 1 }));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function decrement() {
|
|
11
|
+
store.setState((state) => ({ count: Math.max(0, state.count - 1) }));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -9,6 +9,10 @@ type TextDisplayStorage = {
|
|
|
9
9
|
* Whether the typewriter effect is in progress
|
|
10
10
|
*/
|
|
11
11
|
inProgress: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the typewriter effect should be force-completed instantly
|
|
14
|
+
*/
|
|
15
|
+
forceComplete: boolean;
|
|
12
16
|
/**
|
|
13
17
|
* The font size in percent (100 = default)
|
|
14
18
|
*/
|
|
@@ -22,6 +26,7 @@ export namespace TextDisplaySettings {
|
|
|
22
26
|
export const store = new Store<TextDisplayStorage>({
|
|
23
27
|
delay: Number(localStorage.getItem("typewriter_delay_millisecond") ?? 10),
|
|
24
28
|
inProgress: false,
|
|
29
|
+
forceComplete: false,
|
|
25
30
|
fontSize: initialFontSize,
|
|
26
31
|
});
|
|
27
32
|
|
|
@@ -57,6 +62,20 @@ export namespace TextDisplaySettings {
|
|
|
57
62
|
* End the typewriter effect
|
|
58
63
|
*/
|
|
59
64
|
export function end() {
|
|
60
|
-
store.setState((state) => ({ ...state, inProgress: false }));
|
|
65
|
+
store.setState((state) => ({ ...state, inProgress: false, forceComplete: false }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Force the typewriter effect to complete instantly
|
|
70
|
+
*/
|
|
71
|
+
export function complete() {
|
|
72
|
+
store.setState((state) => ({ ...state, forceComplete: true, inProgress: false }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clear the forceComplete flag before advancing to the next step
|
|
77
|
+
*/
|
|
78
|
+
export function resetForNext() {
|
|
79
|
+
store.setState((state) => ({ ...state, forceComplete: false }));
|
|
61
80
|
}
|
|
62
81
|
}
|
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
"menu_navigation_up_description": "Focus the previous menu item.",
|
|
82
82
|
"menu_navigation_home_description": "Focus the first menu item.",
|
|
83
83
|
"menu_navigation_end_description": "Focus the last menu item.",
|
|
84
|
+
"choice_navigation": "Choices",
|
|
85
|
+
"choice_navigation_description": "Use ↑ / ↓ to select an choice, then Enter to run it.",
|
|
84
86
|
"success_delete": "Delete successful",
|
|
85
87
|
"success_load": "Load successful",
|
|
86
88
|
"success_save": "Save successful",
|
|
@@ -12,14 +12,13 @@ import { defineAssets } from "@/lib/utils/assets-utility";
|
|
|
12
12
|
import { initializeIndexedDB } from "@/lib/utils/db-utility";
|
|
13
13
|
import { loadRefreshSave } from "@/lib/utils/save-utility";
|
|
14
14
|
import type { RouterContext } from "@/router";
|
|
15
|
+
import { narration } from "@drincs/pixi-vn";
|
|
15
16
|
import { setupInkHmrListener } from "@drincs/pixi-vn-ink/vite-listener";
|
|
16
17
|
import { setupPixivnViteData } from "@drincs/pixi-vn/vite-listener";
|
|
17
18
|
import { createRootRouteWithContext, ErrorComponent, Outlet } from "@tanstack/react-router";
|
|
18
19
|
import { lazy, Suspense } from "react";
|
|
19
20
|
|
|
20
|
-
const DevDevtools = import.meta.env.DEV
|
|
21
|
-
? lazy(() => import("@/components/dev-devtools"))
|
|
22
|
-
: null;
|
|
21
|
+
const DevDevtools = import.meta.env.DEV ? lazy(() => import("@/components/dev-devtools")) : null;
|
|
23
22
|
|
|
24
23
|
export const Route = createRootRouteWithContext<RouterContext>()({
|
|
25
24
|
validateSearch: (search) => SearchParams.setMany(search),
|
|
@@ -30,7 +29,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|
|
30
29
|
await Promise.all([import("@/content"), initializeIndexedDB(), defineAssets(), useI18n()]);
|
|
31
30
|
await setupPixivnViteData();
|
|
32
31
|
await setupInkHmrListener();
|
|
33
|
-
if (location.pathname !== "/") {
|
|
32
|
+
if (location.pathname !== "/" && narration.stepCounter === 0) {
|
|
34
33
|
const isRefreshSaveExist = await loadRefreshSave();
|
|
35
34
|
if (isRefreshSaveExist) {
|
|
36
35
|
await context.queryClient.invalidateQueries({
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"react-dom": "^19.2.6",
|
|
38
38
|
"react-i18next": "^17.0.8",
|
|
39
39
|
"react-markdown": "^10.1.0",
|
|
40
|
-
"react-markdown-typewriter": "^1.
|
|
40
|
+
"react-markdown-typewriter": "^1.3.4",
|
|
41
41
|
"react-resizable-panels": "^4.11.2",
|
|
42
42
|
"rehype-raw": "^7.0.0",
|
|
43
43
|
"remark-gfm": "^4.0.1",
|
|
@@ -10358,6 +10358,7 @@
|
|
|
10358
10358
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
|
10359
10359
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
|
10360
10360
|
"license": "MIT",
|
|
10361
|
+
"peer": true,
|
|
10361
10362
|
"dependencies": {
|
|
10362
10363
|
"motion-dom": "^12.38.0",
|
|
10363
10364
|
"motion-utils": "^12.36.0",
|
|
@@ -14021,6 +14022,7 @@
|
|
|
14021
14022
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz",
|
|
14022
14023
|
"integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==",
|
|
14023
14024
|
"license": "MIT",
|
|
14025
|
+
"peer": true,
|
|
14024
14026
|
"dependencies": {
|
|
14025
14027
|
"framer-motion": "^12.38.0",
|
|
14026
14028
|
"tslib": "^2.4.0"
|
|
@@ -14047,6 +14049,7 @@
|
|
|
14047
14049
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
|
14048
14050
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
|
14049
14051
|
"license": "MIT",
|
|
14052
|
+
"peer": true,
|
|
14050
14053
|
"dependencies": {
|
|
14051
14054
|
"motion-utils": "^12.36.0"
|
|
14052
14055
|
}
|
|
@@ -14055,7 +14058,8 @@
|
|
|
14055
14058
|
"version": "12.36.0",
|
|
14056
14059
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
|
14057
14060
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
|
14058
|
-
"license": "MIT"
|
|
14061
|
+
"license": "MIT",
|
|
14062
|
+
"peer": true
|
|
14059
14063
|
},
|
|
14060
14064
|
"node_modules/ms": {
|
|
14061
14065
|
"version": "2.1.3",
|
|
@@ -15142,12 +15146,12 @@
|
|
|
15142
15146
|
}
|
|
15143
15147
|
},
|
|
15144
15148
|
"node_modules/react-markdown-typewriter": {
|
|
15145
|
-
"version": "1.
|
|
15146
|
-
"resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.
|
|
15147
|
-
"integrity": "sha512-
|
|
15148
|
-
"
|
|
15149
|
-
"
|
|
15150
|
-
|
|
15149
|
+
"version": "1.3.4",
|
|
15150
|
+
"resolved": "https://registry.npmjs.org/react-markdown-typewriter/-/react-markdown-typewriter-1.3.4.tgz",
|
|
15151
|
+
"integrity": "sha512-LE/7xzyta4A6Gz6CfeTP0saCgVsxbCEVj4le4xobrkM/IG9MVWdrpyGFGFdsKmZ1k5otnRF3HP52YXbMv7P1rg==",
|
|
15152
|
+
"workspaces": [
|
|
15153
|
+
"playground"
|
|
15154
|
+
],
|
|
15151
15155
|
"peerDependencies": {
|
|
15152
15156
|
"motion": ">=12.9.0",
|
|
15153
15157
|
"react": ">=18.0.0",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-dom": "^19.2.6",
|
|
52
52
|
"react-i18next": "^17.0.8",
|
|
53
53
|
"react-markdown": "^10.1.0",
|
|
54
|
-
"react-markdown-typewriter": "^1.
|
|
54
|
+
"react-markdown-typewriter": "^1.3.4",
|
|
55
55
|
"react-resizable-panels": "^4.11.2",
|
|
56
56
|
"rehype-raw": "^7.0.0",
|
|
57
57
|
"remark-gfm": "^4.0.1",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Spinner } from "@/components/ui/spinner";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
2
3
|
|
|
3
4
|
export function PendingComponent() {
|
|
4
5
|
return (
|
|
@@ -12,10 +13,19 @@ export function PendingComponent() {
|
|
|
12
13
|
|
|
13
14
|
export function AnimatedDots() {
|
|
14
15
|
return (
|
|
15
|
-
<span className="flex pointer-events-none select-none">
|
|
16
|
+
<span className="inline-flex pointer-events-none select-none">
|
|
16
17
|
<span className="animate-bounce [animation-delay:0s]">.</span>
|
|
17
18
|
<span className="animate-bounce [animation-delay:0.2s]">.</span>
|
|
18
19
|
<span className="animate-bounce [animation-delay:0.4s]">.</span>
|
|
19
20
|
</span>
|
|
20
21
|
);
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
export function DelayedAnimatedDots({ delay = 300 }: { delay?: number }) {
|
|
25
|
+
const [visible, setVisible] = useState(false);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const id = setTimeout(() => setVisible(true), delay);
|
|
28
|
+
return () => clearTimeout(id);
|
|
29
|
+
}, [delay]);
|
|
30
|
+
return visible ? <AnimatedDots /> : null;
|
|
31
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button } from "@/components/ui/button";
|
|
2
|
+
import { useChoiceMenuHotkeys } from "@/lib/hooks/hotkeys-hooks";
|
|
2
3
|
import { useNarrationFunctions } from "@/lib/hooks/narration-hooks";
|
|
3
4
|
import { useQueryChoiceMenuOptions } from "@/lib/query/narration-query";
|
|
4
5
|
import { GameStatus } from "@/lib/stores/game-status-store";
|
|
@@ -13,9 +14,11 @@ export function ChoiceMenu() {
|
|
|
13
14
|
const isTyping = useSelector(TextDisplaySettings.store, (state) => state.inProgress);
|
|
14
15
|
const { selectChoice } = useNarrationFunctions();
|
|
15
16
|
const [debouncedMenu] = useDebouncedValue(isTyping ? [] : menu, { wait: 50 });
|
|
17
|
+
const { menuRef } = useChoiceMenuHotkeys(debouncedMenu.length);
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<div
|
|
21
|
+
ref={menuRef}
|
|
19
22
|
className="flex flex-col items-center justify-center gap-2 w-full h-full overflow-auto"
|
|
20
23
|
role="menu"
|
|
21
24
|
>
|
|
@@ -28,6 +31,7 @@ export function ChoiceMenu() {
|
|
|
28
31
|
style={{ animationDelay: `${index * 150}ms` }}
|
|
29
32
|
>
|
|
30
33
|
<Button
|
|
34
|
+
role="menuitem"
|
|
31
35
|
disabled={loading}
|
|
32
36
|
onClick={() => selectChoice(item)}
|
|
33
37
|
size="sm"
|
|
@@ -27,7 +27,6 @@ const menuButtonClass =
|
|
|
27
27
|
"justify-start hover:scale-105 focus-visible:scale-105 transition-transform duration-150 ease-out";
|
|
28
28
|
|
|
29
29
|
export function MainMenu() {
|
|
30
|
-
const queryClient = useQueryClient();
|
|
31
30
|
const gameProps = useGameProps();
|
|
32
31
|
const { uiTransition: t, navigate } = gameProps;
|
|
33
32
|
const setSettings = useSetSearchParamState<boolean>("settings");
|
|
@@ -149,11 +148,7 @@ export function MainMenu() {
|
|
|
149
148
|
setLoading(true);
|
|
150
149
|
await navigate({ to: "/game/narration" });
|
|
151
150
|
Game.start("start", gameProps)
|
|
152
|
-
.then(() =>
|
|
153
|
-
queryClient.invalidateQueries({
|
|
154
|
-
queryKey: [INTERFACE_DATA_USE_QUERY_KEY],
|
|
155
|
-
}),
|
|
156
|
-
)
|
|
151
|
+
.then(() => gameProps.invalidateInterfaceData())
|
|
157
152
|
.finally(() => setLoading(false));
|
|
158
153
|
}}
|
|
159
154
|
disabled={loading}
|
|
@@ -208,7 +208,9 @@ export function SettingsDialogue() {
|
|
|
208
208
|
onOpenChange={(isOpen) => {
|
|
209
209
|
setOpen(isOpen || undefined);
|
|
210
210
|
if (!isOpen) {
|
|
211
|
-
|
|
211
|
+
// Delay clearing the tab until after the 100ms close animation
|
|
212
|
+
// to avoid briefly flashing the main settings view during close.
|
|
213
|
+
setTimeout(() => setSettingsTab(undefined), 150);
|
|
212
214
|
}
|
|
213
215
|
}}
|
|
214
216
|
>
|
package/template-react-vite-muijoy-ink-tauri/src/components/modals/input-request-dialogues.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import rehypeRaw from "rehype-raw";
|
|
|
14
14
|
import remarkGfm from "remark-gfm";
|
|
15
15
|
|
|
16
16
|
export function InputRequestDialog() {
|
|
17
|
-
const { data: {
|
|
17
|
+
const { data: { lastText: text } = {} } = useQueryDialogue();
|
|
18
18
|
const {
|
|
19
19
|
data: { isRequired, type, currentValue } = { currentValue: undefined, isRequired: false },
|
|
20
20
|
} = useQueryInputValue<string | number>();
|
|
@@ -223,7 +223,9 @@ function QuickActionsWheelContent({
|
|
|
223
223
|
>
|
|
224
224
|
<span className="font-semibold">{registration.label}</span>
|
|
225
225
|
<span className="text-[11px] opacity-85">
|
|
226
|
-
{registration.hotkeyLabel
|
|
226
|
+
{registration.hotkeyLabel.length > 15
|
|
227
|
+
? `${registration.hotkeyLabel.slice(0, 12)}…`
|
|
228
|
+
: registration.hotkeyLabel}
|
|
227
229
|
</span>
|
|
228
230
|
</button>
|
|
229
231
|
);
|