koin.js 1.0.13 → 1.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/dist/index.js +918 -758
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +919 -759
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React2, { memo, useState, useEffect, useRef, createContext, useMemo, useCallback, useContext } from 'react';
|
|
2
|
-
import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard,
|
|
2
|
+
import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, X, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Lock, Unlock, Move, User, Copy, Zap, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
|
|
3
3
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Nostalgist } from 'nostalgist';
|
|
@@ -1441,6 +1441,40 @@ var PlayerControls = memo(function PlayerControls2({
|
|
|
1441
1441
|
] });
|
|
1442
1442
|
});
|
|
1443
1443
|
var PlayerControls_default = PlayerControls;
|
|
1444
|
+
function useAnimatedVisibility({
|
|
1445
|
+
exitDuration = 200,
|
|
1446
|
+
onExit,
|
|
1447
|
+
autoDismissMs
|
|
1448
|
+
} = {}) {
|
|
1449
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
1450
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
1451
|
+
useEffect(() => {
|
|
1452
|
+
requestAnimationFrame(() => {
|
|
1453
|
+
setIsVisible(true);
|
|
1454
|
+
});
|
|
1455
|
+
}, []);
|
|
1456
|
+
useEffect(() => {
|
|
1457
|
+
if (!autoDismissMs) return;
|
|
1458
|
+
const timer = setTimeout(() => {
|
|
1459
|
+
triggerExit();
|
|
1460
|
+
}, autoDismissMs);
|
|
1461
|
+
return () => clearTimeout(timer);
|
|
1462
|
+
}, [autoDismissMs]);
|
|
1463
|
+
const triggerExit = useCallback(() => {
|
|
1464
|
+
if (isExiting) return;
|
|
1465
|
+
setIsExiting(true);
|
|
1466
|
+
setTimeout(() => {
|
|
1467
|
+
onExit?.();
|
|
1468
|
+
}, exitDuration);
|
|
1469
|
+
}, [isExiting, exitDuration, onExit]);
|
|
1470
|
+
const slideInRightClasses = isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0";
|
|
1471
|
+
return {
|
|
1472
|
+
isVisible,
|
|
1473
|
+
isExiting,
|
|
1474
|
+
triggerExit,
|
|
1475
|
+
slideInRightClasses
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1444
1478
|
var TOAST_CONFIGS = {
|
|
1445
1479
|
success: {
|
|
1446
1480
|
icon: CheckCircle,
|
|
@@ -1485,32 +1519,20 @@ var TOAST_CONFIGS = {
|
|
|
1485
1519
|
}
|
|
1486
1520
|
};
|
|
1487
1521
|
function ToastItem({ toast, onDismiss }) {
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1522
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
1523
|
+
exitDuration: 200,
|
|
1524
|
+
onExit: () => onDismiss?.(toast.id)
|
|
1525
|
+
});
|
|
1490
1526
|
const config = TOAST_CONFIGS[toast.type];
|
|
1491
1527
|
const IconComponent = config.icon;
|
|
1492
|
-
useEffect(() => {
|
|
1493
|
-
requestAnimationFrame(() => {
|
|
1494
|
-
setIsVisible(true);
|
|
1495
|
-
});
|
|
1496
|
-
}, []);
|
|
1497
|
-
const handleDismiss = () => {
|
|
1498
|
-
setIsExiting(true);
|
|
1499
|
-
setTimeout(() => {
|
|
1500
|
-
onDismiss?.(toast.id);
|
|
1501
|
-
}, 200);
|
|
1502
|
-
};
|
|
1503
1528
|
return /* @__PURE__ */ jsx(
|
|
1504
1529
|
"div",
|
|
1505
1530
|
{
|
|
1506
|
-
className: `
|
|
1507
|
-
relative transition-all duration-300 ease-out
|
|
1508
|
-
${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
|
|
1509
|
-
`,
|
|
1531
|
+
className: `relative transition-all duration-300 ease-out ${slideInRightClasses}`,
|
|
1510
1532
|
children: /* @__PURE__ */ jsxs(
|
|
1511
1533
|
"div",
|
|
1512
1534
|
{
|
|
1513
|
-
className: "relative w-[320px]",
|
|
1535
|
+
className: "relative w-[320px] pointer-events-auto",
|
|
1514
1536
|
style: {
|
|
1515
1537
|
backgroundColor: config.bgColor,
|
|
1516
1538
|
border: `2px solid ${config.borderColor}`,
|
|
@@ -1545,7 +1567,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1545
1567
|
{
|
|
1546
1568
|
onClick: () => {
|
|
1547
1569
|
toast.action?.onClick();
|
|
1548
|
-
|
|
1570
|
+
triggerExit();
|
|
1549
1571
|
},
|
|
1550
1572
|
className: "flex-shrink-0 text-[9px] font-black uppercase tracking-wider px-2 py-1 transition-all hover:-translate-y-0.5 active:translate-y-0",
|
|
1551
1573
|
style: {
|
|
@@ -1559,7 +1581,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1559
1581
|
/* @__PURE__ */ jsx(
|
|
1560
1582
|
"button",
|
|
1561
1583
|
{
|
|
1562
|
-
onClick:
|
|
1584
|
+
onClick: triggerExit,
|
|
1563
1585
|
className: "flex-shrink-0 p-0.5 text-gray-500 hover:text-white transition-colors",
|
|
1564
1586
|
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
1565
1587
|
}
|
|
@@ -1932,6 +1954,67 @@ var RecordingIndicator = memo(function RecordingIndicator2({
|
|
|
1932
1954
|
);
|
|
1933
1955
|
});
|
|
1934
1956
|
var RecordingIndicator_default = RecordingIndicator;
|
|
1957
|
+
var MAX_WIDTH_CLASSES = {
|
|
1958
|
+
sm: "max-w-sm",
|
|
1959
|
+
md: "max-w-md",
|
|
1960
|
+
lg: "max-w-lg"
|
|
1961
|
+
};
|
|
1962
|
+
function ModalShell({
|
|
1963
|
+
isOpen,
|
|
1964
|
+
onClose,
|
|
1965
|
+
title,
|
|
1966
|
+
subtitle,
|
|
1967
|
+
icon,
|
|
1968
|
+
children,
|
|
1969
|
+
footer,
|
|
1970
|
+
maxWidth = "lg",
|
|
1971
|
+
systemColor,
|
|
1972
|
+
closeOnBackdrop = true
|
|
1973
|
+
}) {
|
|
1974
|
+
if (!isOpen) return null;
|
|
1975
|
+
const handleBackdropClick = () => {
|
|
1976
|
+
if (closeOnBackdrop) {
|
|
1977
|
+
onClose();
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1981
|
+
/* @__PURE__ */ jsx(
|
|
1982
|
+
"div",
|
|
1983
|
+
{
|
|
1984
|
+
className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
|
|
1985
|
+
onClick: handleBackdropClick
|
|
1986
|
+
}
|
|
1987
|
+
),
|
|
1988
|
+
/* @__PURE__ */ jsxs(
|
|
1989
|
+
"div",
|
|
1990
|
+
{
|
|
1991
|
+
className: `relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full ${MAX_WIDTH_CLASSES[maxWidth]} mx-4 overflow-hidden`,
|
|
1992
|
+
style: systemColor ? { borderColor: `${systemColor}30` } : void 0,
|
|
1993
|
+
children: [
|
|
1994
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
|
|
1995
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1996
|
+
/* @__PURE__ */ jsx("div", { style: systemColor ? { color: systemColor } : void 0, children: icon }),
|
|
1997
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1998
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: title }),
|
|
1999
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: subtitle })
|
|
2000
|
+
] })
|
|
2001
|
+
] }),
|
|
2002
|
+
/* @__PURE__ */ jsx(
|
|
2003
|
+
"button",
|
|
2004
|
+
{
|
|
2005
|
+
onClick: onClose,
|
|
2006
|
+
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
2007
|
+
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
2008
|
+
}
|
|
2009
|
+
)
|
|
2010
|
+
] }),
|
|
2011
|
+
children,
|
|
2012
|
+
footer && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: footer })
|
|
2013
|
+
]
|
|
2014
|
+
}
|
|
2015
|
+
)
|
|
2016
|
+
] });
|
|
2017
|
+
}
|
|
1935
2018
|
var ShortcutsModal = memo(function ShortcutsModal2({
|
|
1936
2019
|
isOpen,
|
|
1937
2020
|
onClose,
|
|
@@ -1960,90 +2043,55 @@ var ShortcutsModal = memo(function ShortcutsModal2({
|
|
|
1960
2043
|
]
|
|
1961
2044
|
}
|
|
1962
2045
|
], [t]);
|
|
1963
|
-
if (!isOpen) return null;
|
|
1964
2046
|
return /* @__PURE__ */ jsx(
|
|
1965
|
-
|
|
2047
|
+
ModalShell,
|
|
1966
2048
|
{
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
className: "p-1 rounded hover:bg-white/10 transition-colors",
|
|
1991
|
-
children: /* @__PURE__ */ jsx(X, { size: 18, className: "text-white/60 hover:text-white" })
|
|
1992
|
-
}
|
|
1993
|
-
)
|
|
1994
|
-
]
|
|
1995
|
-
}
|
|
1996
|
-
),
|
|
1997
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
|
|
1998
|
-
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2049
|
+
isOpen,
|
|
2050
|
+
onClose,
|
|
2051
|
+
title: t.modals.shortcuts.playerShortcuts,
|
|
2052
|
+
icon: /* @__PURE__ */ jsx(Keyboard, { size: 20, style: { color: systemColor } }),
|
|
2053
|
+
maxWidth: "sm",
|
|
2054
|
+
systemColor,
|
|
2055
|
+
footer: /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 w-full text-center", children: t.modals.shortcuts.pressEsc }),
|
|
2056
|
+
children: /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
|
|
2057
|
+
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2058
|
+
/* @__PURE__ */ jsx(
|
|
2059
|
+
"h3",
|
|
2060
|
+
{
|
|
2061
|
+
className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
|
|
2062
|
+
style: { color: systemColor },
|
|
2063
|
+
children: section
|
|
2064
|
+
}
|
|
2065
|
+
),
|
|
2066
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxs(
|
|
2067
|
+
"div",
|
|
2068
|
+
{
|
|
2069
|
+
className: "flex items-center justify-between text-sm",
|
|
2070
|
+
children: [
|
|
2071
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/70", children: description }),
|
|
1999
2072
|
/* @__PURE__ */ jsx(
|
|
2000
|
-
"
|
|
2073
|
+
"kbd",
|
|
2001
2074
|
{
|
|
2002
|
-
className: "
|
|
2003
|
-
style: {
|
|
2004
|
-
|
|
2075
|
+
className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
|
|
2076
|
+
style: {
|
|
2077
|
+
backgroundColor: `${systemColor}20`,
|
|
2078
|
+
color: systemColor,
|
|
2079
|
+
border: `1px solid ${systemColor}40`
|
|
2080
|
+
},
|
|
2081
|
+
children: key
|
|
2005
2082
|
}
|
|
2006
|
-
)
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
backgroundColor: `${systemColor}20`,
|
|
2019
|
-
color: systemColor,
|
|
2020
|
-
border: `1px solid ${systemColor}40`
|
|
2021
|
-
},
|
|
2022
|
-
children: key
|
|
2023
|
-
}
|
|
2024
|
-
)
|
|
2025
|
-
]
|
|
2026
|
-
},
|
|
2027
|
-
key
|
|
2028
|
-
)) })
|
|
2029
|
-
] }, section)),
|
|
2030
|
-
/* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2031
|
-
"Game controls can be configured in ",
|
|
2032
|
-
/* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2033
|
-
" settings."
|
|
2034
|
-
] })
|
|
2035
|
-
] }),
|
|
2036
|
-
/* @__PURE__ */ jsx(
|
|
2037
|
-
"div",
|
|
2038
|
-
{
|
|
2039
|
-
className: "px-4 py-2 text-center text-xs text-white/40 border-t",
|
|
2040
|
-
style: { borderColor: `${systemColor}20` },
|
|
2041
|
-
children: t.modals.shortcuts.pressEsc
|
|
2042
|
-
}
|
|
2043
|
-
)
|
|
2044
|
-
]
|
|
2045
|
-
}
|
|
2046
|
-
)
|
|
2083
|
+
)
|
|
2084
|
+
]
|
|
2085
|
+
},
|
|
2086
|
+
key
|
|
2087
|
+
)) })
|
|
2088
|
+
] }, section)),
|
|
2089
|
+
/* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2090
|
+
"Game controls can be configured in ",
|
|
2091
|
+
/* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2092
|
+
" settings."
|
|
2093
|
+
] })
|
|
2094
|
+
] })
|
|
2047
2095
|
}
|
|
2048
2096
|
);
|
|
2049
2097
|
});
|
|
@@ -2387,26 +2435,49 @@ function getLayoutForSystem(system) {
|
|
|
2387
2435
|
if (s.includes("ATARI")) return TWO_BUTTON_LAYOUT;
|
|
2388
2436
|
return TWO_BUTTON_LAYOUT;
|
|
2389
2437
|
}
|
|
2390
|
-
|
|
2438
|
+
|
|
2439
|
+
// src/components/VirtualController/utils/dragConstraints.ts
|
|
2440
|
+
function constrainToViewport({
|
|
2441
|
+
newXPercent,
|
|
2442
|
+
newYPercent,
|
|
2443
|
+
elementSize,
|
|
2444
|
+
containerWidth,
|
|
2445
|
+
containerHeight
|
|
2446
|
+
}) {
|
|
2447
|
+
const xMargin = elementSize / 2 / containerWidth * 100;
|
|
2448
|
+
const yMargin = elementSize / 2 / containerHeight * 100;
|
|
2449
|
+
return {
|
|
2450
|
+
x: Math.max(xMargin, Math.min(100 - xMargin, newXPercent)),
|
|
2451
|
+
y: Math.max(yMargin, Math.min(100 - yMargin, newYPercent))
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// src/components/VirtualController/hooks/useDrag.ts
|
|
2456
|
+
var DEFAULT_HOLD_DELAY = 350;
|
|
2457
|
+
var DEFAULT_CENTER_THRESHOLD = 0.4;
|
|
2391
2458
|
var DRAG_MOVE_THRESHOLD = 10;
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
buttonType,
|
|
2395
|
-
isSystemButton,
|
|
2396
|
-
buttonSize,
|
|
2459
|
+
function useDrag({
|
|
2460
|
+
elementSize,
|
|
2397
2461
|
displayX,
|
|
2398
2462
|
displayY,
|
|
2399
2463
|
containerWidth,
|
|
2400
2464
|
containerHeight,
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2465
|
+
onPositionChange,
|
|
2466
|
+
holdDelay = DEFAULT_HOLD_DELAY,
|
|
2467
|
+
centerThreshold = DEFAULT_CENTER_THRESHOLD,
|
|
2468
|
+
onDragStart,
|
|
2469
|
+
onDragEnd
|
|
2405
2470
|
}) {
|
|
2471
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
2406
2472
|
const isDraggingRef = useRef(false);
|
|
2407
|
-
const dragStartRef = useRef({ x: 0, y: 0 });
|
|
2408
2473
|
const dragTimerRef = useRef(null);
|
|
2409
2474
|
const touchStartPosRef = useRef({ x: 0, y: 0 });
|
|
2475
|
+
const dragStartRef = useRef({
|
|
2476
|
+
elementX: 0,
|
|
2477
|
+
elementY: 0,
|
|
2478
|
+
touchX: 0,
|
|
2479
|
+
touchY: 0
|
|
2480
|
+
});
|
|
2410
2481
|
const clearDragTimer = useCallback(() => {
|
|
2411
2482
|
if (dragTimerRef.current) {
|
|
2412
2483
|
clearTimeout(dragTimerRef.current);
|
|
@@ -2416,23 +2487,129 @@ function useTouchHandlers({
|
|
|
2416
2487
|
const startDragging = useCallback(
|
|
2417
2488
|
(touchX, touchY) => {
|
|
2418
2489
|
isDraggingRef.current = true;
|
|
2490
|
+
setIsDragging(true);
|
|
2419
2491
|
dragStartRef.current = {
|
|
2420
|
-
|
|
2421
|
-
|
|
2492
|
+
elementX: displayX,
|
|
2493
|
+
elementY: displayY,
|
|
2494
|
+
touchX,
|
|
2495
|
+
touchY
|
|
2422
2496
|
};
|
|
2423
2497
|
if (navigator.vibrate) {
|
|
2424
2498
|
navigator.vibrate([10, 30, 10]);
|
|
2425
2499
|
}
|
|
2500
|
+
onDragStart?.();
|
|
2501
|
+
},
|
|
2502
|
+
[displayX, displayY, onDragStart]
|
|
2503
|
+
);
|
|
2504
|
+
const checkDragStart = useCallback(
|
|
2505
|
+
(touchX, touchY, elementRect) => {
|
|
2506
|
+
if (!onPositionChange) return false;
|
|
2507
|
+
touchStartPosRef.current = { x: touchX, y: touchY };
|
|
2508
|
+
const centerX = elementRect.left + elementRect.width / 2;
|
|
2509
|
+
const centerY = elementRect.top + elementRect.height / 2;
|
|
2510
|
+
const distFromCenter = Math.sqrt(
|
|
2511
|
+
Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2)
|
|
2512
|
+
);
|
|
2513
|
+
const centerRadius = elementSize * centerThreshold;
|
|
2514
|
+
if (distFromCenter < centerRadius) {
|
|
2515
|
+
dragTimerRef.current = setTimeout(() => {
|
|
2516
|
+
if (!isDraggingRef.current) {
|
|
2517
|
+
startDragging(touchX, touchY);
|
|
2518
|
+
}
|
|
2519
|
+
}, holdDelay);
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
return false;
|
|
2523
|
+
},
|
|
2524
|
+
[onPositionChange, elementSize, centerThreshold, holdDelay, startDragging]
|
|
2525
|
+
);
|
|
2526
|
+
const checkMoveThreshold = useCallback(
|
|
2527
|
+
(touchX, touchY) => {
|
|
2528
|
+
if (!onPositionChange || isDraggingRef.current) return false;
|
|
2529
|
+
const moveDistance = Math.sqrt(
|
|
2530
|
+
Math.pow(touchX - touchStartPosRef.current.x, 2) + Math.pow(touchY - touchStartPosRef.current.y, 2)
|
|
2531
|
+
);
|
|
2532
|
+
if (moveDistance > DRAG_MOVE_THRESHOLD) {
|
|
2533
|
+
clearDragTimer();
|
|
2534
|
+
startDragging(touchX, touchY);
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
return false;
|
|
2538
|
+
},
|
|
2539
|
+
[onPositionChange, clearDragTimer, startDragging]
|
|
2540
|
+
);
|
|
2541
|
+
const handleDragMove = useCallback(
|
|
2542
|
+
(touchX, touchY) => {
|
|
2543
|
+
if (!isDraggingRef.current || !onPositionChange) return;
|
|
2544
|
+
const deltaX = touchX - dragStartRef.current.touchX;
|
|
2545
|
+
const deltaY = touchY - dragStartRef.current.touchY;
|
|
2546
|
+
const newXPercent = dragStartRef.current.elementX + deltaX / containerWidth * 100;
|
|
2547
|
+
const newYPercent = dragStartRef.current.elementY + deltaY / containerHeight * 100;
|
|
2548
|
+
const constrained = constrainToViewport({
|
|
2549
|
+
newXPercent,
|
|
2550
|
+
newYPercent,
|
|
2551
|
+
elementSize,
|
|
2552
|
+
containerWidth,
|
|
2553
|
+
containerHeight
|
|
2554
|
+
});
|
|
2555
|
+
onPositionChange(constrained.x, constrained.y);
|
|
2556
|
+
},
|
|
2557
|
+
[onPositionChange, containerWidth, containerHeight, elementSize]
|
|
2558
|
+
);
|
|
2559
|
+
const handleDragEnd = useCallback(() => {
|
|
2560
|
+
clearDragTimer();
|
|
2561
|
+
if (isDraggingRef.current) {
|
|
2562
|
+
isDraggingRef.current = false;
|
|
2563
|
+
setIsDragging(false);
|
|
2564
|
+
onDragEnd?.();
|
|
2565
|
+
}
|
|
2566
|
+
}, [clearDragTimer, onDragEnd]);
|
|
2567
|
+
return {
|
|
2568
|
+
isDragging,
|
|
2569
|
+
checkDragStart,
|
|
2570
|
+
handleDragMove,
|
|
2571
|
+
handleDragEnd,
|
|
2572
|
+
clearDragTimer,
|
|
2573
|
+
checkMoveThreshold
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// src/components/VirtualController/hooks/useTouchHandlers.ts
|
|
2578
|
+
function useTouchHandlers({
|
|
2579
|
+
buttonType,
|
|
2580
|
+
isSystemButton,
|
|
2581
|
+
buttonSize,
|
|
2582
|
+
displayX,
|
|
2583
|
+
displayY,
|
|
2584
|
+
containerWidth,
|
|
2585
|
+
containerHeight,
|
|
2586
|
+
onPress,
|
|
2587
|
+
onPressDown,
|
|
2588
|
+
onRelease,
|
|
2589
|
+
onPositionChange
|
|
2590
|
+
}) {
|
|
2591
|
+
const isDraggingRef = useRef(false);
|
|
2592
|
+
const drag = useDrag({
|
|
2593
|
+
elementSize: buttonSize,
|
|
2594
|
+
displayX,
|
|
2595
|
+
displayY,
|
|
2596
|
+
containerWidth,
|
|
2597
|
+
containerHeight,
|
|
2598
|
+
onPositionChange,
|
|
2599
|
+
centerThreshold: 0.4,
|
|
2600
|
+
onDragStart: () => {
|
|
2601
|
+
isDraggingRef.current = true;
|
|
2426
2602
|
if (!isSystemButton) {
|
|
2427
2603
|
onRelease(buttonType);
|
|
2428
2604
|
}
|
|
2429
2605
|
},
|
|
2430
|
-
|
|
2431
|
-
|
|
2606
|
+
onDragEnd: () => {
|
|
2607
|
+
isDraggingRef.current = false;
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2432
2610
|
const handleTouchStart = useCallback(
|
|
2433
2611
|
(e) => {
|
|
2434
2612
|
const touch = e.touches[0];
|
|
2435
|
-
touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
|
2436
2613
|
e.preventDefault();
|
|
2437
2614
|
e.stopPropagation();
|
|
2438
2615
|
if (navigator.vibrate) {
|
|
@@ -2447,57 +2624,32 @@ function useTouchHandlers({
|
|
|
2447
2624
|
const target = e.currentTarget;
|
|
2448
2625
|
if (!target) return;
|
|
2449
2626
|
const rect = target.getBoundingClientRect();
|
|
2450
|
-
|
|
2451
|
-
const centerY = rect.top + rect.height / 2;
|
|
2452
|
-
const distance = Math.sqrt(
|
|
2453
|
-
Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
|
|
2454
|
-
);
|
|
2455
|
-
const dragThreshold = buttonSize * DRAG_CENTER_THRESHOLD;
|
|
2456
|
-
if (distance < dragThreshold) {
|
|
2457
|
-
dragTimerRef.current = setTimeout(() => {
|
|
2458
|
-
if (!isDraggingRef.current) {
|
|
2459
|
-
startDragging(touch.clientX, touch.clientY);
|
|
2460
|
-
}
|
|
2461
|
-
}, DRAG_HOLD_DELAY);
|
|
2462
|
-
}
|
|
2627
|
+
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
2463
2628
|
}
|
|
2464
2629
|
},
|
|
2465
|
-
[isSystemButton, buttonType, onPress, onPressDown, onPositionChange,
|
|
2630
|
+
[isSystemButton, buttonType, onPress, onPressDown, onPositionChange, drag]
|
|
2466
2631
|
);
|
|
2467
2632
|
const handleTouchMove = useCallback(
|
|
2468
2633
|
(e) => {
|
|
2469
2634
|
const touch = e.touches[0];
|
|
2470
2635
|
if (onPositionChange && !isDraggingRef.current) {
|
|
2471
|
-
|
|
2472
|
-
Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
|
|
2473
|
-
);
|
|
2474
|
-
if (moveDistance > DRAG_MOVE_THRESHOLD) {
|
|
2475
|
-
clearDragTimer();
|
|
2476
|
-
startDragging(touch.clientX, touch.clientY);
|
|
2477
|
-
}
|
|
2636
|
+
drag.checkMoveThreshold(touch.clientX, touch.clientY);
|
|
2478
2637
|
}
|
|
2479
|
-
if (isDraggingRef.current
|
|
2638
|
+
if (isDraggingRef.current) {
|
|
2480
2639
|
e.preventDefault();
|
|
2481
2640
|
e.stopPropagation();
|
|
2482
|
-
|
|
2483
|
-
const newY = touch.clientY - dragStartRef.current.y;
|
|
2484
|
-
const newXPercent = newX / containerWidth * 100;
|
|
2485
|
-
const newYPercent = newY / containerHeight * 100;
|
|
2486
|
-
const margin = buttonSize / 2 / Math.min(containerWidth, containerHeight) * 100;
|
|
2487
|
-
const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
|
|
2488
|
-
const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
|
|
2489
|
-
onPositionChange(constrainedX, constrainedY);
|
|
2641
|
+
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
2490
2642
|
}
|
|
2491
2643
|
},
|
|
2492
|
-
[onPositionChange,
|
|
2644
|
+
[onPositionChange, drag]
|
|
2493
2645
|
);
|
|
2494
2646
|
const handleTouchEnd = useCallback(
|
|
2495
2647
|
(e) => {
|
|
2496
|
-
clearDragTimer();
|
|
2648
|
+
drag.clearDragTimer();
|
|
2497
2649
|
if (isDraggingRef.current) {
|
|
2498
2650
|
e.preventDefault();
|
|
2499
2651
|
e.stopPropagation();
|
|
2500
|
-
|
|
2652
|
+
drag.handleDragEnd();
|
|
2501
2653
|
return;
|
|
2502
2654
|
}
|
|
2503
2655
|
e.preventDefault();
|
|
@@ -2506,15 +2658,15 @@ function useTouchHandlers({
|
|
|
2506
2658
|
onRelease(buttonType);
|
|
2507
2659
|
}
|
|
2508
2660
|
},
|
|
2509
|
-
[
|
|
2661
|
+
[drag, isSystemButton, buttonType, onRelease]
|
|
2510
2662
|
);
|
|
2511
2663
|
const handleTouchCancel = useCallback(
|
|
2512
2664
|
(e) => {
|
|
2513
|
-
clearDragTimer();
|
|
2665
|
+
drag.clearDragTimer();
|
|
2514
2666
|
if (isDraggingRef.current) {
|
|
2515
2667
|
e.preventDefault();
|
|
2516
2668
|
e.stopPropagation();
|
|
2517
|
-
|
|
2669
|
+
drag.handleDragEnd();
|
|
2518
2670
|
return;
|
|
2519
2671
|
}
|
|
2520
2672
|
e.preventDefault();
|
|
@@ -2523,11 +2675,11 @@ function useTouchHandlers({
|
|
|
2523
2675
|
onRelease(buttonType);
|
|
2524
2676
|
}
|
|
2525
2677
|
},
|
|
2526
|
-
[
|
|
2678
|
+
[drag, isSystemButton, buttonType, onRelease]
|
|
2527
2679
|
);
|
|
2528
2680
|
const cleanup = useCallback(() => {
|
|
2529
|
-
clearDragTimer();
|
|
2530
|
-
}, [
|
|
2681
|
+
drag.clearDragTimer();
|
|
2682
|
+
}, [drag]);
|
|
2531
2683
|
return {
|
|
2532
2684
|
handleTouchStart,
|
|
2533
2685
|
handleTouchMove,
|
|
@@ -2536,6 +2688,39 @@ function useTouchHandlers({
|
|
|
2536
2688
|
cleanup
|
|
2537
2689
|
};
|
|
2538
2690
|
}
|
|
2691
|
+
function useTouchEvents(ref, handlers, options = {}) {
|
|
2692
|
+
const { cleanup, passive = false } = options;
|
|
2693
|
+
const handlersRef = useRef(handlers);
|
|
2694
|
+
handlersRef.current = handlers;
|
|
2695
|
+
const handleTouchStart = useCallback((e) => {
|
|
2696
|
+
handlersRef.current.onTouchStart?.(e);
|
|
2697
|
+
}, []);
|
|
2698
|
+
const handleTouchMove = useCallback((e) => {
|
|
2699
|
+
handlersRef.current.onTouchMove?.(e);
|
|
2700
|
+
}, []);
|
|
2701
|
+
const handleTouchEnd = useCallback((e) => {
|
|
2702
|
+
handlersRef.current.onTouchEnd?.(e);
|
|
2703
|
+
}, []);
|
|
2704
|
+
const handleTouchCancel = useCallback((e) => {
|
|
2705
|
+
handlersRef.current.onTouchCancel?.(e);
|
|
2706
|
+
}, []);
|
|
2707
|
+
useEffect(() => {
|
|
2708
|
+
const element = ref.current;
|
|
2709
|
+
if (!element) return;
|
|
2710
|
+
const listenerOptions = { passive };
|
|
2711
|
+
element.addEventListener("touchstart", handleTouchStart, listenerOptions);
|
|
2712
|
+
element.addEventListener("touchmove", handleTouchMove, listenerOptions);
|
|
2713
|
+
element.addEventListener("touchend", handleTouchEnd, listenerOptions);
|
|
2714
|
+
element.addEventListener("touchcancel", handleTouchCancel, listenerOptions);
|
|
2715
|
+
return () => {
|
|
2716
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
2717
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
2718
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
2719
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
2720
|
+
cleanup?.();
|
|
2721
|
+
};
|
|
2722
|
+
}, [ref, passive, cleanup, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
2723
|
+
}
|
|
2539
2724
|
|
|
2540
2725
|
// src/components/VirtualController/utils/buttonStyles.ts
|
|
2541
2726
|
var DEFAULT_FACE = {
|
|
@@ -2693,21 +2878,12 @@ var VirtualButton = React2.memo(function VirtualButton2({
|
|
|
2693
2878
|
onRelease,
|
|
2694
2879
|
onPositionChange
|
|
2695
2880
|
});
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
|
|
2703
|
-
return () => {
|
|
2704
|
-
button.removeEventListener("touchstart", handleTouchStart);
|
|
2705
|
-
button.removeEventListener("touchmove", handleTouchMove);
|
|
2706
|
-
button.removeEventListener("touchend", handleTouchEnd);
|
|
2707
|
-
button.removeEventListener("touchcancel", handleTouchCancel);
|
|
2708
|
-
cleanup();
|
|
2709
|
-
};
|
|
2710
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
|
|
2881
|
+
useTouchEvents(buttonRef, {
|
|
2882
|
+
onTouchStart: handleTouchStart,
|
|
2883
|
+
onTouchMove: handleTouchMove,
|
|
2884
|
+
onTouchEnd: handleTouchEnd,
|
|
2885
|
+
onTouchCancel: handleTouchCancel
|
|
2886
|
+
}, { cleanup });
|
|
2711
2887
|
const leftPercent = displayX / 100 * containerWidth - config.size / 2;
|
|
2712
2888
|
const topPercent = displayY / 100 * containerHeight - config.size / 2;
|
|
2713
2889
|
const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
|
|
@@ -3383,8 +3559,7 @@ function dispatchKeyboardEvent(type, code) {
|
|
|
3383
3559
|
canvas.dispatchEvent(event);
|
|
3384
3560
|
return true;
|
|
3385
3561
|
}
|
|
3386
|
-
var
|
|
3387
|
-
var CENTER_TOUCH_RADIUS = 0.25;
|
|
3562
|
+
var CENTER_TOUCH_RADIUS = 0.35;
|
|
3388
3563
|
var Dpad = React2.memo(function Dpad2({
|
|
3389
3564
|
size = 180,
|
|
3390
3565
|
x,
|
|
@@ -3400,10 +3575,6 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3400
3575
|
const dpadRef = useRef(null);
|
|
3401
3576
|
const activeTouchRef = useRef(null);
|
|
3402
3577
|
const activeDirectionsRef = useRef(/* @__PURE__ */ new Set());
|
|
3403
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
3404
|
-
const dragTimerRef = useRef(null);
|
|
3405
|
-
const dragStartRef = useRef({ x: 0, y: 0, touchX: 0, touchY: 0 });
|
|
3406
|
-
const touchStartPosRef = useRef({ x: 0, y: 0, time: 0 });
|
|
3407
3578
|
const upPathRef = useRef(null);
|
|
3408
3579
|
const downPathRef = useRef(null);
|
|
3409
3580
|
const leftPathRef = useRef(null);
|
|
@@ -3411,6 +3582,13 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3411
3582
|
const centerCircleRef = useRef(null);
|
|
3412
3583
|
const displayX = customPosition ? customPosition.x : x;
|
|
3413
3584
|
const displayY = customPosition ? customPosition.y : y;
|
|
3585
|
+
const releaseAllDirections = useCallback((getKeyCode2) => {
|
|
3586
|
+
activeDirectionsRef.current.forEach((dir) => {
|
|
3587
|
+
const keyCode = getKeyCode2(dir);
|
|
3588
|
+
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3589
|
+
});
|
|
3590
|
+
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3591
|
+
}, []);
|
|
3414
3592
|
const getKeyCode = useCallback((direction) => {
|
|
3415
3593
|
if (!controls) {
|
|
3416
3594
|
const defaults = {
|
|
@@ -3423,6 +3601,19 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3423
3601
|
}
|
|
3424
3602
|
return controls[direction] || "";
|
|
3425
3603
|
}, [controls]);
|
|
3604
|
+
const drag = useDrag({
|
|
3605
|
+
elementSize: size,
|
|
3606
|
+
displayX,
|
|
3607
|
+
displayY,
|
|
3608
|
+
containerWidth,
|
|
3609
|
+
containerHeight,
|
|
3610
|
+
onPositionChange,
|
|
3611
|
+
centerThreshold: CENTER_TOUCH_RADIUS,
|
|
3612
|
+
onDragStart: () => {
|
|
3613
|
+
releaseAllDirections(getKeyCode);
|
|
3614
|
+
updateVisuals(/* @__PURE__ */ new Set());
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3426
3617
|
const getDirectionsFromTouch = useCallback((touchX, touchY, rect) => {
|
|
3427
3618
|
const centerX = rect.left + rect.width / 2;
|
|
3428
3619
|
const centerY = rect.top + rect.height / 2;
|
|
@@ -3484,51 +3675,20 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3484
3675
|
activeDirectionsRef.current = newDirections;
|
|
3485
3676
|
updateVisuals(newDirections);
|
|
3486
3677
|
}, [getKeyCode, updateVisuals]);
|
|
3487
|
-
const clearDragTimer = useCallback(() => {
|
|
3488
|
-
if (dragTimerRef.current) {
|
|
3489
|
-
clearTimeout(dragTimerRef.current);
|
|
3490
|
-
dragTimerRef.current = null;
|
|
3491
|
-
}
|
|
3492
|
-
}, []);
|
|
3493
|
-
const startDragging = useCallback((touchX, touchY) => {
|
|
3494
|
-
setIsDragging(true);
|
|
3495
|
-
dragStartRef.current = {
|
|
3496
|
-
x: displayX,
|
|
3497
|
-
y: displayY,
|
|
3498
|
-
touchX,
|
|
3499
|
-
touchY
|
|
3500
|
-
};
|
|
3501
|
-
activeDirectionsRef.current.forEach((dir) => {
|
|
3502
|
-
const keyCode = getKeyCode(dir);
|
|
3503
|
-
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3504
|
-
});
|
|
3505
|
-
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3506
|
-
updateVisuals(/* @__PURE__ */ new Set());
|
|
3507
|
-
if (navigator.vibrate) navigator.vibrate([10, 30, 10]);
|
|
3508
|
-
}, [displayX, displayY, getKeyCode, updateVisuals]);
|
|
3509
3678
|
const handleTouchStart = useCallback((e) => {
|
|
3510
3679
|
e.preventDefault();
|
|
3511
3680
|
if (activeTouchRef.current !== null) return;
|
|
3512
3681
|
const touch = e.changedTouches[0];
|
|
3513
3682
|
activeTouchRef.current = touch.identifier;
|
|
3514
|
-
touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
3515
3683
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3516
3684
|
if (!rect) return;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
const distFromCenter = Math.sqrt(
|
|
3520
|
-
Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
|
|
3521
|
-
);
|
|
3522
|
-
const centerRadius = size * CENTER_TOUCH_RADIUS;
|
|
3523
|
-
if (distFromCenter < centerRadius && onPositionChange) {
|
|
3524
|
-
dragTimerRef.current = setTimeout(() => {
|
|
3525
|
-
startDragging(touch.clientX, touch.clientY);
|
|
3526
|
-
}, DRAG_HOLD_DELAY2);
|
|
3685
|
+
if (onPositionChange) {
|
|
3686
|
+
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
3527
3687
|
}
|
|
3528
|
-
if (!isDragging) {
|
|
3688
|
+
if (!drag.isDragging) {
|
|
3529
3689
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3530
3690
|
}
|
|
3531
|
-
}, [getDirectionsFromTouch, updateDirections,
|
|
3691
|
+
}, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
|
|
3532
3692
|
const handleTouchMove = useCallback((e) => {
|
|
3533
3693
|
e.preventDefault();
|
|
3534
3694
|
let touch = null;
|
|
@@ -3539,31 +3699,19 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3539
3699
|
}
|
|
3540
3700
|
}
|
|
3541
3701
|
if (!touch) return;
|
|
3542
|
-
if (isDragging
|
|
3543
|
-
|
|
3544
|
-
const deltaY = touch.clientY - dragStartRef.current.touchY;
|
|
3545
|
-
const newXPercent = dragStartRef.current.x + deltaX / containerWidth * 100;
|
|
3546
|
-
const newYPercent = dragStartRef.current.y + deltaY / containerHeight * 100;
|
|
3547
|
-
const margin = size / 2 / Math.min(containerWidth, containerHeight) * 100;
|
|
3548
|
-
const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
|
|
3549
|
-
const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
|
|
3550
|
-
onPositionChange(constrainedX, constrainedY);
|
|
3702
|
+
if (drag.isDragging) {
|
|
3703
|
+
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3551
3704
|
} else {
|
|
3552
|
-
const moveDistance = Math.sqrt(
|
|
3553
|
-
Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
|
|
3554
|
-
);
|
|
3555
|
-
if (moveDistance > 15) {
|
|
3556
|
-
clearDragTimer();
|
|
3557
|
-
}
|
|
3558
3705
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3559
3706
|
if (rect) {
|
|
3707
|
+
drag.clearDragTimer();
|
|
3560
3708
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3561
3709
|
}
|
|
3562
3710
|
}
|
|
3563
|
-
}, [
|
|
3711
|
+
}, [drag, getDirectionsFromTouch, updateDirections]);
|
|
3564
3712
|
const handleTouchEnd = useCallback((e) => {
|
|
3565
3713
|
e.preventDefault();
|
|
3566
|
-
clearDragTimer();
|
|
3714
|
+
drag.clearDragTimer();
|
|
3567
3715
|
let touchEnded = false;
|
|
3568
3716
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3569
3717
|
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
@@ -3573,8 +3721,8 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3573
3721
|
}
|
|
3574
3722
|
if (touchEnded) {
|
|
3575
3723
|
activeTouchRef.current = null;
|
|
3576
|
-
if (isDragging) {
|
|
3577
|
-
|
|
3724
|
+
if (drag.isDragging) {
|
|
3725
|
+
drag.handleDragEnd();
|
|
3578
3726
|
} else {
|
|
3579
3727
|
activeDirectionsRef.current.forEach((dir) => {
|
|
3580
3728
|
const keyCode = getKeyCode(dir);
|
|
@@ -3584,22 +3732,13 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3584
3732
|
updateVisuals(/* @__PURE__ */ new Set());
|
|
3585
3733
|
}
|
|
3586
3734
|
}
|
|
3587
|
-
}, [getKeyCode, updateVisuals,
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
|
|
3595
|
-
return () => {
|
|
3596
|
-
dpad.removeEventListener("touchstart", handleTouchStart);
|
|
3597
|
-
dpad.removeEventListener("touchmove", handleTouchMove);
|
|
3598
|
-
dpad.removeEventListener("touchend", handleTouchEnd);
|
|
3599
|
-
dpad.removeEventListener("touchcancel", handleTouchEnd);
|
|
3600
|
-
clearDragTimer();
|
|
3601
|
-
};
|
|
3602
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, clearDragTimer]);
|
|
3735
|
+
}, [getKeyCode, updateVisuals, drag]);
|
|
3736
|
+
useTouchEvents(dpadRef, {
|
|
3737
|
+
onTouchStart: handleTouchStart,
|
|
3738
|
+
onTouchMove: handleTouchMove,
|
|
3739
|
+
onTouchEnd: handleTouchEnd,
|
|
3740
|
+
onTouchCancel: handleTouchEnd
|
|
3741
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3603
3742
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3604
3743
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3605
3744
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3610,21 +3749,21 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3610
3749
|
"div",
|
|
3611
3750
|
{
|
|
3612
3751
|
ref: dpadRef,
|
|
3613
|
-
className: `absolute pointer-events-auto touch-manipulation select-none ${isDragging ? "opacity-60" : ""}`,
|
|
3752
|
+
className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
|
|
3614
3753
|
style: {
|
|
3615
3754
|
top: 0,
|
|
3616
3755
|
left: 0,
|
|
3617
|
-
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${isDragging ? " scale(1.05)" : ""}`,
|
|
3756
|
+
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
|
|
3618
3757
|
width: size,
|
|
3619
3758
|
height: size,
|
|
3620
3759
|
opacity: isLandscape ? 0.75 : 0.9,
|
|
3621
3760
|
WebkitTouchCallout: "none",
|
|
3622
3761
|
WebkitUserSelect: "none",
|
|
3623
3762
|
touchAction: "none",
|
|
3624
|
-
transition: isDragging ? "none" : "transform 0.1s ease-out"
|
|
3763
|
+
transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
|
|
3625
3764
|
},
|
|
3626
3765
|
children: [
|
|
3627
|
-
/* @__PURE__ */ jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3766
|
+
/* @__PURE__ */ jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${drag.isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3628
3767
|
/* @__PURE__ */ jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
|
|
3629
3768
|
/* @__PURE__ */ jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3630
3769
|
/* @__PURE__ */ jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
@@ -3637,9 +3776,9 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3637
3776
|
cx: "50",
|
|
3638
3777
|
cy: "50",
|
|
3639
3778
|
r: "12",
|
|
3640
|
-
fill: isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3641
|
-
stroke: isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3642
|
-
strokeWidth: isDragging ? 2 : 1
|
|
3779
|
+
fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3780
|
+
stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3781
|
+
strokeWidth: drag.isDragging ? 2 : 1
|
|
3643
3782
|
}
|
|
3644
3783
|
),
|
|
3645
3784
|
/* @__PURE__ */ jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
@@ -3799,6 +3938,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3799
3938
|
}
|
|
3800
3939
|
);
|
|
3801
3940
|
}
|
|
3941
|
+
function LockButton({
|
|
3942
|
+
isLocked,
|
|
3943
|
+
onToggle,
|
|
3944
|
+
systemColor = "#00FF41"
|
|
3945
|
+
}) {
|
|
3946
|
+
const Icon = isLocked ? Lock : Unlock;
|
|
3947
|
+
return /* @__PURE__ */ jsx(
|
|
3948
|
+
"button",
|
|
3949
|
+
{
|
|
3950
|
+
onClick: onToggle,
|
|
3951
|
+
className: "fixed top-4 left-1/2 -translate-x-1/2 z-40 pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
|
|
3952
|
+
style: {
|
|
3953
|
+
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3954
|
+
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
3955
|
+
},
|
|
3956
|
+
"aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
|
|
3957
|
+
children: /* @__PURE__ */ jsx(
|
|
3958
|
+
Icon,
|
|
3959
|
+
{
|
|
3960
|
+
size: 18,
|
|
3961
|
+
style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
|
|
3962
|
+
}
|
|
3963
|
+
)
|
|
3964
|
+
}
|
|
3965
|
+
);
|
|
3966
|
+
}
|
|
3967
|
+
var LOCK_KEY = "koin-controls-locked";
|
|
3802
3968
|
function VirtualController({
|
|
3803
3969
|
system,
|
|
3804
3970
|
isRunning,
|
|
@@ -3810,8 +3976,22 @@ function VirtualController({
|
|
|
3810
3976
|
const [pressedButtons, setPressedButtons] = useState(/* @__PURE__ */ new Set());
|
|
3811
3977
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
3812
3978
|
const [isFullscreenState, setIsFullscreenState] = useState(false);
|
|
3979
|
+
const [isLocked, setIsLocked] = useState(true);
|
|
3813
3980
|
const { getPosition, savePosition } = useButtonPositions();
|
|
3814
|
-
|
|
3981
|
+
useEffect(() => {
|
|
3982
|
+
const stored = localStorage.getItem(LOCK_KEY);
|
|
3983
|
+
if (stored !== null) {
|
|
3984
|
+
setIsLocked(stored === "true");
|
|
3985
|
+
}
|
|
3986
|
+
}, []);
|
|
3987
|
+
const toggleLock = useCallback(() => {
|
|
3988
|
+
setIsLocked((prev) => {
|
|
3989
|
+
const newValue = !prev;
|
|
3990
|
+
localStorage.setItem(LOCK_KEY, String(newValue));
|
|
3991
|
+
return newValue;
|
|
3992
|
+
});
|
|
3993
|
+
}, []);
|
|
3994
|
+
const layout = getLayoutForSystem(system);
|
|
3815
3995
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3816
3996
|
if (isPortrait) {
|
|
3817
3997
|
return btn.showInPortrait;
|
|
@@ -3965,6 +4145,14 @@ function VirtualController({
|
|
|
3965
4145
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
3966
4146
|
style: { touchAction: "none" },
|
|
3967
4147
|
children: [
|
|
4148
|
+
/* @__PURE__ */ jsx(
|
|
4149
|
+
LockButton,
|
|
4150
|
+
{
|
|
4151
|
+
isLocked,
|
|
4152
|
+
onToggle: toggleLock,
|
|
4153
|
+
systemColor
|
|
4154
|
+
}
|
|
4155
|
+
),
|
|
3968
4156
|
/* @__PURE__ */ jsx(
|
|
3969
4157
|
Dpad_default,
|
|
3970
4158
|
{
|
|
@@ -3977,7 +4165,7 @@ function VirtualController({
|
|
|
3977
4165
|
systemColor,
|
|
3978
4166
|
isLandscape,
|
|
3979
4167
|
customPosition: getPosition("up", isLandscape),
|
|
3980
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4168
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
3981
4169
|
}
|
|
3982
4170
|
),
|
|
3983
4171
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
|
|
@@ -3991,7 +4179,7 @@ function VirtualController({
|
|
|
3991
4179
|
containerWidth: width,
|
|
3992
4180
|
containerHeight: height,
|
|
3993
4181
|
customPosition,
|
|
3994
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4182
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
3995
4183
|
isLandscape,
|
|
3996
4184
|
console: layout.console
|
|
3997
4185
|
},
|
|
@@ -4067,6 +4255,45 @@ function FloatingFullscreenButton({ onClick, disabled = false }) {
|
|
|
4067
4255
|
}
|
|
4068
4256
|
);
|
|
4069
4257
|
}
|
|
4258
|
+
function FloatingPauseButton({
|
|
4259
|
+
isPaused,
|
|
4260
|
+
onClick,
|
|
4261
|
+
disabled = false,
|
|
4262
|
+
systemColor = "#00FF41"
|
|
4263
|
+
}) {
|
|
4264
|
+
return /* @__PURE__ */ jsx(
|
|
4265
|
+
"button",
|
|
4266
|
+
{
|
|
4267
|
+
onClick,
|
|
4268
|
+
disabled,
|
|
4269
|
+
className: `
|
|
4270
|
+
fixed top-3 left-3 z-50
|
|
4271
|
+
px-3 py-2 rounded-xl
|
|
4272
|
+
bg-black/80 backdrop-blur-md
|
|
4273
|
+
border-2
|
|
4274
|
+
shadow-xl
|
|
4275
|
+
flex items-center gap-2
|
|
4276
|
+
transition-all duration-300
|
|
4277
|
+
hover:scale-105
|
|
4278
|
+
active:scale-95
|
|
4279
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
4280
|
+
touch-manipulation
|
|
4281
|
+
`,
|
|
4282
|
+
style: {
|
|
4283
|
+
paddingTop: "max(env(safe-area-inset-top, 0px), 8px)",
|
|
4284
|
+
borderColor: isPaused ? systemColor : "rgba(255,255,255,0.3)"
|
|
4285
|
+
},
|
|
4286
|
+
"aria-label": isPaused ? "Resume game" : "Pause game",
|
|
4287
|
+
children: isPaused ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4288
|
+
/* @__PURE__ */ jsx(Play, { size: 16, style: { color: systemColor }, fill: systemColor }),
|
|
4289
|
+
/* @__PURE__ */ jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Play" })
|
|
4290
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4291
|
+
/* @__PURE__ */ jsx(Pause, { size: 16, className: "text-white/80" }),
|
|
4292
|
+
/* @__PURE__ */ jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Pause" })
|
|
4293
|
+
] })
|
|
4294
|
+
}
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4070
4297
|
function LoadingSpinner({ color, size = "lg" }) {
|
|
4071
4298
|
const sizeClass = size === "lg" ? "w-12 h-12" : "w-8 h-8";
|
|
4072
4299
|
return /* @__PURE__ */ jsx(Loader2, { className: `${sizeClass} animate-spin`, style: { color } });
|
|
@@ -4272,6 +4499,45 @@ var GameCanvas = memo(function GameCanvas2({
|
|
|
4272
4499
|
] });
|
|
4273
4500
|
});
|
|
4274
4501
|
var GameCanvas_default = GameCanvas;
|
|
4502
|
+
function useInputCapture({
|
|
4503
|
+
isOpen,
|
|
4504
|
+
onClose
|
|
4505
|
+
}) {
|
|
4506
|
+
const [listeningFor, setListeningFor] = useState(null);
|
|
4507
|
+
const startListening = useCallback((target) => {
|
|
4508
|
+
setListeningFor(target);
|
|
4509
|
+
}, []);
|
|
4510
|
+
const stopListening = useCallback(() => {
|
|
4511
|
+
setListeningFor(null);
|
|
4512
|
+
}, []);
|
|
4513
|
+
useEffect(() => {
|
|
4514
|
+
if (!isOpen) {
|
|
4515
|
+
setListeningFor(null);
|
|
4516
|
+
}
|
|
4517
|
+
}, [isOpen]);
|
|
4518
|
+
useEffect(() => {
|
|
4519
|
+
if (!isOpen) return;
|
|
4520
|
+
const handleKeyDown = (e) => {
|
|
4521
|
+
if (e.code === "Escape") {
|
|
4522
|
+
if (listeningFor !== null) {
|
|
4523
|
+
e.preventDefault();
|
|
4524
|
+
e.stopPropagation();
|
|
4525
|
+
setListeningFor(null);
|
|
4526
|
+
} else {
|
|
4527
|
+
onClose();
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4532
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4533
|
+
}, [isOpen, listeningFor, onClose]);
|
|
4534
|
+
return {
|
|
4535
|
+
listeningFor,
|
|
4536
|
+
startListening,
|
|
4537
|
+
stopListening,
|
|
4538
|
+
isListening: listeningFor !== null
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4275
4541
|
function getFilteredGroups(activeButtons) {
|
|
4276
4542
|
return BUTTON_GROUPS.map((group) => ({
|
|
4277
4543
|
...group,
|
|
@@ -4287,7 +4553,10 @@ function ControlMapper({
|
|
|
4287
4553
|
}) {
|
|
4288
4554
|
const t = useKoinTranslation();
|
|
4289
4555
|
const [localControls, setLocalControls] = useState(controls);
|
|
4290
|
-
const
|
|
4556
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4557
|
+
isOpen,
|
|
4558
|
+
onClose
|
|
4559
|
+
});
|
|
4291
4560
|
const activeButtons = useMemo(() => {
|
|
4292
4561
|
return getConsoleButtons(system || "SNES");
|
|
4293
4562
|
}, [system]);
|
|
@@ -4303,27 +4572,20 @@ function ControlMapper({
|
|
|
4303
4572
|
}
|
|
4304
4573
|
}, [isOpen, controls]);
|
|
4305
4574
|
useEffect(() => {
|
|
4306
|
-
if (!isOpen)
|
|
4307
|
-
setListeningFor(null);
|
|
4308
|
-
return;
|
|
4309
|
-
}
|
|
4575
|
+
if (!isOpen || !listeningFor) return;
|
|
4310
4576
|
const handleKeyDown = (e) => {
|
|
4311
|
-
if (
|
|
4577
|
+
if (e.code === "Escape") return;
|
|
4312
4578
|
e.preventDefault();
|
|
4313
4579
|
e.stopPropagation();
|
|
4314
|
-
if (e.code === "Escape") {
|
|
4315
|
-
setListeningFor(null);
|
|
4316
|
-
return;
|
|
4317
|
-
}
|
|
4318
4580
|
setLocalControls((prev) => ({
|
|
4319
4581
|
...prev,
|
|
4320
4582
|
[listeningFor]: e.code
|
|
4321
4583
|
}));
|
|
4322
|
-
|
|
4584
|
+
stopListening();
|
|
4323
4585
|
};
|
|
4324
4586
|
window.addEventListener("keydown", handleKeyDown);
|
|
4325
4587
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4326
|
-
}, [isOpen, listeningFor]);
|
|
4588
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4327
4589
|
const handleReset = () => {
|
|
4328
4590
|
setLocalControls(defaultControls);
|
|
4329
4591
|
};
|
|
@@ -4331,52 +4593,58 @@ function ControlMapper({
|
|
|
4331
4593
|
onSave(localControls);
|
|
4332
4594
|
onClose();
|
|
4333
4595
|
};
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
}
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4596
|
+
return /* @__PURE__ */ jsx(
|
|
4597
|
+
ModalShell,
|
|
4598
|
+
{
|
|
4599
|
+
isOpen,
|
|
4600
|
+
onClose,
|
|
4601
|
+
title: t.modals.controls.title,
|
|
4602
|
+
subtitle: t.modals.controls.description,
|
|
4603
|
+
icon: /* @__PURE__ */ jsx(Gamepad2, { className: "text-retro-primary", size: 24 }),
|
|
4604
|
+
closeOnBackdrop: !isListening,
|
|
4605
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4606
|
+
/* @__PURE__ */ jsxs(
|
|
4607
|
+
"button",
|
|
4608
|
+
{
|
|
4609
|
+
onClick: handleReset,
|
|
4610
|
+
className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
|
|
4611
|
+
children: [
|
|
4612
|
+
/* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
|
|
4613
|
+
t.modals.controls.reset
|
|
4614
|
+
]
|
|
4615
|
+
}
|
|
4616
|
+
),
|
|
4617
|
+
/* @__PURE__ */ jsxs(
|
|
4353
4618
|
"button",
|
|
4354
4619
|
{
|
|
4355
|
-
onClick:
|
|
4356
|
-
className: "
|
|
4357
|
-
children:
|
|
4620
|
+
onClick: handleSave,
|
|
4621
|
+
className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
|
|
4622
|
+
children: [
|
|
4623
|
+
/* @__PURE__ */ jsx(Check, { size: 16 }),
|
|
4624
|
+
t.modals.controls.save
|
|
4625
|
+
]
|
|
4358
4626
|
}
|
|
4359
4627
|
)
|
|
4360
4628
|
] }),
|
|
4361
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4629
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4362
4630
|
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4363
4631
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4364
4632
|
"button",
|
|
4365
4633
|
{
|
|
4366
|
-
onClick: () =>
|
|
4634
|
+
onClick: () => startListening(btn),
|
|
4367
4635
|
className: `
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4636
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4637
|
+
${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4638
|
+
`,
|
|
4371
4639
|
children: [
|
|
4372
4640
|
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4373
4641
|
/* @__PURE__ */ jsx(
|
|
4374
4642
|
"span",
|
|
4375
4643
|
{
|
|
4376
4644
|
className: `
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4645
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4646
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4647
|
+
`,
|
|
4380
4648
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4381
4649
|
}
|
|
4382
4650
|
)
|
|
@@ -4384,199 +4652,10 @@ function ControlMapper({
|
|
|
4384
4652
|
},
|
|
4385
4653
|
btn
|
|
4386
4654
|
)) })
|
|
4387
|
-
] }, group.label)) })
|
|
4388
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4389
|
-
/* @__PURE__ */ jsxs(
|
|
4390
|
-
"button",
|
|
4391
|
-
{
|
|
4392
|
-
onClick: handleReset,
|
|
4393
|
-
className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
|
|
4394
|
-
children: [
|
|
4395
|
-
/* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
|
|
4396
|
-
t.modals.controls.reset
|
|
4397
|
-
]
|
|
4398
|
-
}
|
|
4399
|
-
),
|
|
4400
|
-
/* @__PURE__ */ jsxs(
|
|
4401
|
-
"button",
|
|
4402
|
-
{
|
|
4403
|
-
onClick: handleSave,
|
|
4404
|
-
className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
|
|
4405
|
-
children: [
|
|
4406
|
-
/* @__PURE__ */ jsx(Check, { size: 16 }),
|
|
4407
|
-
t.modals.controls.save
|
|
4408
|
-
]
|
|
4409
|
-
}
|
|
4410
|
-
)
|
|
4411
|
-
] })
|
|
4412
|
-
] })
|
|
4413
|
-
] });
|
|
4414
|
-
}
|
|
4415
|
-
function getDisplayName(id) {
|
|
4416
|
-
let name = id;
|
|
4417
|
-
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
4418
|
-
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
4419
|
-
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
4420
|
-
if (/xbox/i.test(name)) {
|
|
4421
|
-
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
4422
|
-
if (/one/i.test(name)) return "Xbox One Controller";
|
|
4423
|
-
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
4424
|
-
return "Xbox Controller";
|
|
4425
|
-
}
|
|
4426
|
-
if (/dualsense/i.test(name)) return "DualSense";
|
|
4427
|
-
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
4428
|
-
if (/dualshock/i.test(name)) return "DualShock";
|
|
4429
|
-
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
4430
|
-
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
4431
|
-
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
4432
|
-
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
4433
|
-
return name.trim() || "Gamepad";
|
|
4434
|
-
}
|
|
4435
|
-
function detectControllerBrand(id) {
|
|
4436
|
-
const lowerId = id.toLowerCase();
|
|
4437
|
-
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
4438
|
-
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
4439
|
-
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
4440
|
-
return "generic";
|
|
4441
|
-
}
|
|
4442
|
-
function toGamepadInfo(gamepad) {
|
|
4443
|
-
return {
|
|
4444
|
-
index: gamepad.index,
|
|
4445
|
-
id: gamepad.id,
|
|
4446
|
-
name: getDisplayName(gamepad.id),
|
|
4447
|
-
connected: gamepad.connected,
|
|
4448
|
-
buttons: gamepad.buttons.length,
|
|
4449
|
-
axes: gamepad.axes.length,
|
|
4450
|
-
mapping: gamepad.mapping
|
|
4451
|
-
};
|
|
4452
|
-
}
|
|
4453
|
-
function useGamepad(options) {
|
|
4454
|
-
const { onConnect, onDisconnect } = options || {};
|
|
4455
|
-
const [gamepads, setGamepads] = useState([]);
|
|
4456
|
-
const rafRef = useRef(null);
|
|
4457
|
-
const lastStateRef = useRef("");
|
|
4458
|
-
const prevCountRef = useRef(0);
|
|
4459
|
-
const onConnectRef = useRef(onConnect);
|
|
4460
|
-
const onDisconnectRef = useRef(onDisconnect);
|
|
4461
|
-
useEffect(() => {
|
|
4462
|
-
onConnectRef.current = onConnect;
|
|
4463
|
-
onDisconnectRef.current = onDisconnect;
|
|
4464
|
-
}, [onConnect, onDisconnect]);
|
|
4465
|
-
const getGamepads = useCallback(() => {
|
|
4466
|
-
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
4467
|
-
return [];
|
|
4468
|
-
}
|
|
4469
|
-
const rawGamepads = navigator.getGamepads() ?? [];
|
|
4470
|
-
const connected = [];
|
|
4471
|
-
for (let i = 0; i < rawGamepads.length; i++) {
|
|
4472
|
-
const gp = rawGamepads[i];
|
|
4473
|
-
if (gp && gp.connected) {
|
|
4474
|
-
connected.push(toGamepadInfo(gp));
|
|
4475
|
-
}
|
|
4476
|
-
}
|
|
4477
|
-
return connected;
|
|
4478
|
-
}, []);
|
|
4479
|
-
const getRawGamepad = useCallback((index) => {
|
|
4480
|
-
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
4481
|
-
return rawGamepads[index] ?? null;
|
|
4482
|
-
}, []);
|
|
4483
|
-
const refresh = useCallback(() => {
|
|
4484
|
-
setGamepads(getGamepads());
|
|
4485
|
-
}, [getGamepads]);
|
|
4486
|
-
useEffect(() => {
|
|
4487
|
-
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
4488
|
-
return;
|
|
4489
|
-
}
|
|
4490
|
-
if (typeof navigator.getGamepads !== "function") {
|
|
4491
|
-
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
4492
|
-
return;
|
|
4655
|
+
] }, group.label)) })
|
|
4493
4656
|
}
|
|
4494
|
-
|
|
4495
|
-
const poll = () => {
|
|
4496
|
-
if (!isActive) return;
|
|
4497
|
-
const current = getGamepads();
|
|
4498
|
-
let hasChanged = current.length !== prevCountRef.current;
|
|
4499
|
-
if (!hasChanged) {
|
|
4500
|
-
for (let i = 0; i < current.length; i++) {
|
|
4501
|
-
const saved = gamepads[i];
|
|
4502
|
-
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
4503
|
-
hasChanged = true;
|
|
4504
|
-
break;
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
|
-
}
|
|
4508
|
-
if (hasChanged) {
|
|
4509
|
-
const prevCount = prevCountRef.current;
|
|
4510
|
-
const currentCount = current.length;
|
|
4511
|
-
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
4512
|
-
const newGamepad = current[current.length - 1];
|
|
4513
|
-
onConnectRef.current(newGamepad);
|
|
4514
|
-
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
4515
|
-
onDisconnectRef.current();
|
|
4516
|
-
}
|
|
4517
|
-
prevCountRef.current = currentCount;
|
|
4518
|
-
setGamepads(current);
|
|
4519
|
-
}
|
|
4520
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4521
|
-
};
|
|
4522
|
-
const handleConnect = (e) => {
|
|
4523
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
4524
|
-
const current = getGamepads();
|
|
4525
|
-
const prevCount = prevCountRef.current;
|
|
4526
|
-
prevCountRef.current = current.length;
|
|
4527
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4528
|
-
setGamepads(current);
|
|
4529
|
-
if (onConnectRef.current && current.length > prevCount) {
|
|
4530
|
-
const newGamepad = current[current.length - 1];
|
|
4531
|
-
onConnectRef.current(newGamepad);
|
|
4532
|
-
}
|
|
4533
|
-
};
|
|
4534
|
-
const handleDisconnect = (e) => {
|
|
4535
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
4536
|
-
const current = getGamepads();
|
|
4537
|
-
const prevCount = prevCountRef.current;
|
|
4538
|
-
prevCountRef.current = current.length;
|
|
4539
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4540
|
-
setGamepads(current);
|
|
4541
|
-
if (onDisconnectRef.current && current.length < prevCount) {
|
|
4542
|
-
onDisconnectRef.current();
|
|
4543
|
-
}
|
|
4544
|
-
};
|
|
4545
|
-
window.addEventListener("gamepadconnected", handleConnect);
|
|
4546
|
-
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
4547
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4548
|
-
const initial = getGamepads();
|
|
4549
|
-
if (initial.length > 0) {
|
|
4550
|
-
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
4551
|
-
prevCountRef.current = initial.length;
|
|
4552
|
-
setGamepads(initial);
|
|
4553
|
-
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4554
|
-
} else {
|
|
4555
|
-
prevCountRef.current = 0;
|
|
4556
|
-
}
|
|
4557
|
-
return () => {
|
|
4558
|
-
isActive = false;
|
|
4559
|
-
if (rafRef.current) {
|
|
4560
|
-
cancelAnimationFrame(rafRef.current);
|
|
4561
|
-
}
|
|
4562
|
-
window.removeEventListener("gamepadconnected", handleConnect);
|
|
4563
|
-
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
4564
|
-
};
|
|
4565
|
-
}, [getGamepads]);
|
|
4566
|
-
return {
|
|
4567
|
-
gamepads,
|
|
4568
|
-
isAnyConnected: gamepads.length > 0,
|
|
4569
|
-
connectedCount: gamepads.length,
|
|
4570
|
-
getRawGamepad,
|
|
4571
|
-
refresh
|
|
4572
|
-
};
|
|
4657
|
+
);
|
|
4573
4658
|
}
|
|
4574
|
-
var STANDARD_AXIS_MAP = {
|
|
4575
|
-
leftStickX: 0,
|
|
4576
|
-
leftStickY: 1,
|
|
4577
|
-
rightStickX: 2,
|
|
4578
|
-
rightStickY: 3
|
|
4579
|
-
};
|
|
4580
4659
|
function GamepadMapper({
|
|
4581
4660
|
isOpen,
|
|
4582
4661
|
gamepads,
|
|
@@ -4587,7 +4666,10 @@ function GamepadMapper({
|
|
|
4587
4666
|
const t = useKoinTranslation();
|
|
4588
4667
|
const [selectedPlayer, setSelectedPlayer] = useState(1);
|
|
4589
4668
|
const [bindings, setBindings] = useState({});
|
|
4590
|
-
const
|
|
4669
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4670
|
+
isOpen,
|
|
4671
|
+
onClose
|
|
4672
|
+
});
|
|
4591
4673
|
const rafRef = useRef(null);
|
|
4592
4674
|
useEffect(() => {
|
|
4593
4675
|
if (isOpen) {
|
|
@@ -4622,7 +4704,7 @@ function GamepadMapper({
|
|
|
4622
4704
|
[listeningFor]: i
|
|
4623
4705
|
}
|
|
4624
4706
|
}));
|
|
4625
|
-
|
|
4707
|
+
stopListening();
|
|
4626
4708
|
return;
|
|
4627
4709
|
}
|
|
4628
4710
|
}
|
|
@@ -4635,21 +4717,7 @@ function GamepadMapper({
|
|
|
4635
4717
|
cancelAnimationFrame(rafRef.current);
|
|
4636
4718
|
}
|
|
4637
4719
|
};
|
|
4638
|
-
}, [isOpen, listeningFor, selectedPlayer]);
|
|
4639
|
-
useEffect(() => {
|
|
4640
|
-
if (!isOpen) return;
|
|
4641
|
-
const handleKeyDown = (e) => {
|
|
4642
|
-
if (e.code === "Escape") {
|
|
4643
|
-
if (listeningFor) {
|
|
4644
|
-
setListeningFor(null);
|
|
4645
|
-
} else {
|
|
4646
|
-
onClose();
|
|
4647
|
-
}
|
|
4648
|
-
}
|
|
4649
|
-
};
|
|
4650
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
4651
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4652
|
-
}, [isOpen, listeningFor, onClose]);
|
|
4720
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4653
4721
|
const handleReset = () => {
|
|
4654
4722
|
setBindings((prev) => ({
|
|
4655
4723
|
...prev,
|
|
@@ -4664,127 +4732,19 @@ function GamepadMapper({
|
|
|
4664
4732
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4665
4733
|
onClose();
|
|
4666
4734
|
};
|
|
4667
|
-
if (!isOpen) return null;
|
|
4668
4735
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4669
4736
|
const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
}
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
/* @__PURE__ */ jsxs(
|
|
4681
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4682
|
-
/* @__PURE__ */ jsx(Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
|
|
4683
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
4684
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
|
|
4685
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none })
|
|
4686
|
-
] })
|
|
4687
|
-
] }),
|
|
4688
|
-
/* @__PURE__ */ jsx(
|
|
4689
|
-
"button",
|
|
4690
|
-
{
|
|
4691
|
-
onClick: onClose,
|
|
4692
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4693
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
4694
|
-
}
|
|
4695
|
-
)
|
|
4696
|
-
] }),
|
|
4697
|
-
gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4698
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4699
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4700
|
-
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
|
|
4701
|
-
"button",
|
|
4702
|
-
{
|
|
4703
|
-
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4704
|
-
className: `
|
|
4705
|
-
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4706
|
-
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4707
|
-
`,
|
|
4708
|
-
style: selectedPlayer === gp.index + 1 ? {
|
|
4709
|
-
backgroundColor: `${systemColor}20`,
|
|
4710
|
-
color: systemColor
|
|
4711
|
-
} : {},
|
|
4712
|
-
children: [
|
|
4713
|
-
/* @__PURE__ */ jsx(User, { size: 14 }),
|
|
4714
|
-
"P",
|
|
4715
|
-
gp.index + 1
|
|
4716
|
-
]
|
|
4717
|
-
},
|
|
4718
|
-
gp.index
|
|
4719
|
-
)) })
|
|
4720
|
-
] }),
|
|
4721
|
-
currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4722
|
-
] }),
|
|
4723
|
-
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4724
|
-
currentGamepad.name,
|
|
4725
|
-
" \u2022 Player 1"
|
|
4726
|
-
] }) }),
|
|
4727
|
-
gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4728
|
-
/* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4729
|
-
/* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4730
|
-
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4731
|
-
] }),
|
|
4732
|
-
/* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4733
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4734
|
-
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4735
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4736
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
4737
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4738
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4739
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4740
|
-
] })
|
|
4741
|
-
] })
|
|
4742
|
-
] }),
|
|
4743
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4744
|
-
listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4745
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4746
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4747
|
-
] }),
|
|
4748
|
-
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4749
|
-
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4750
|
-
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4751
|
-
"button",
|
|
4752
|
-
{
|
|
4753
|
-
onClick: () => setListeningFor(btn),
|
|
4754
|
-
disabled: !!listeningFor && listeningFor !== btn,
|
|
4755
|
-
className: `
|
|
4756
|
-
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4757
|
-
disabled:opacity-50
|
|
4758
|
-
${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4759
|
-
`,
|
|
4760
|
-
style: listeningFor === btn ? {
|
|
4761
|
-
borderColor: systemColor,
|
|
4762
|
-
backgroundColor: `${systemColor}20`,
|
|
4763
|
-
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4764
|
-
} : {},
|
|
4765
|
-
children: [
|
|
4766
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4767
|
-
/* @__PURE__ */ jsx(
|
|
4768
|
-
"span",
|
|
4769
|
-
{
|
|
4770
|
-
className: `
|
|
4771
|
-
px-2 py-1 rounded text-xs font-mono
|
|
4772
|
-
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4773
|
-
`,
|
|
4774
|
-
style: listeningFor === btn ? {
|
|
4775
|
-
backgroundColor: `${systemColor}30`,
|
|
4776
|
-
color: systemColor
|
|
4777
|
-
} : {},
|
|
4778
|
-
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4779
|
-
}
|
|
4780
|
-
)
|
|
4781
|
-
]
|
|
4782
|
-
},
|
|
4783
|
-
btn
|
|
4784
|
-
)) })
|
|
4785
|
-
] }, group.label))
|
|
4786
|
-
] }),
|
|
4787
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4737
|
+
return /* @__PURE__ */ jsxs(
|
|
4738
|
+
ModalShell,
|
|
4739
|
+
{
|
|
4740
|
+
isOpen,
|
|
4741
|
+
onClose,
|
|
4742
|
+
title: t.modals.gamepad.title,
|
|
4743
|
+
subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
|
|
4744
|
+
icon: /* @__PURE__ */ jsx(Joystick, { size: 24, style: { color: systemColor } }),
|
|
4745
|
+
systemColor,
|
|
4746
|
+
closeOnBackdrop: !isListening,
|
|
4747
|
+
footer: gamepads.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4788
4748
|
/* @__PURE__ */ jsxs(
|
|
4789
4749
|
"button",
|
|
4790
4750
|
{
|
|
@@ -4810,9 +4770,101 @@ function GamepadMapper({
|
|
|
4810
4770
|
]
|
|
4811
4771
|
}
|
|
4812
4772
|
)
|
|
4813
|
-
] })
|
|
4814
|
-
|
|
4815
|
-
|
|
4773
|
+
] }) : void 0,
|
|
4774
|
+
children: [
|
|
4775
|
+
gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4776
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4777
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4778
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
|
|
4779
|
+
"button",
|
|
4780
|
+
{
|
|
4781
|
+
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4782
|
+
className: `
|
|
4783
|
+
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4784
|
+
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4785
|
+
`,
|
|
4786
|
+
style: selectedPlayer === gp.index + 1 ? {
|
|
4787
|
+
backgroundColor: `${systemColor}20`,
|
|
4788
|
+
color: systemColor
|
|
4789
|
+
} : {},
|
|
4790
|
+
children: [
|
|
4791
|
+
/* @__PURE__ */ jsx(User, { size: 14 }),
|
|
4792
|
+
"P",
|
|
4793
|
+
gp.index + 1
|
|
4794
|
+
]
|
|
4795
|
+
},
|
|
4796
|
+
gp.index
|
|
4797
|
+
)) })
|
|
4798
|
+
] }),
|
|
4799
|
+
currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4800
|
+
] }),
|
|
4801
|
+
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4802
|
+
currentGamepad.name,
|
|
4803
|
+
" \u2022 Player 1"
|
|
4804
|
+
] }) }),
|
|
4805
|
+
gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4806
|
+
/* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4807
|
+
/* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4808
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4809
|
+
] }),
|
|
4810
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4811
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4812
|
+
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4813
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4814
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
4815
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4816
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4817
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4818
|
+
] })
|
|
4819
|
+
] })
|
|
4820
|
+
] }),
|
|
4821
|
+
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4822
|
+
listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4823
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4824
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4825
|
+
] }),
|
|
4826
|
+
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4827
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4828
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4829
|
+
"button",
|
|
4830
|
+
{
|
|
4831
|
+
onClick: () => startListening(btn),
|
|
4832
|
+
disabled: !!listeningFor && listeningFor !== btn,
|
|
4833
|
+
className: `
|
|
4834
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4835
|
+
disabled:opacity-50
|
|
4836
|
+
${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4837
|
+
`,
|
|
4838
|
+
style: listeningFor === btn ? {
|
|
4839
|
+
borderColor: systemColor,
|
|
4840
|
+
backgroundColor: `${systemColor}20`,
|
|
4841
|
+
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4842
|
+
} : {},
|
|
4843
|
+
children: [
|
|
4844
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4845
|
+
/* @__PURE__ */ jsx(
|
|
4846
|
+
"span",
|
|
4847
|
+
{
|
|
4848
|
+
className: `
|
|
4849
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4850
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4851
|
+
`,
|
|
4852
|
+
style: listeningFor === btn ? {
|
|
4853
|
+
backgroundColor: `${systemColor}30`,
|
|
4854
|
+
color: systemColor
|
|
4855
|
+
} : {},
|
|
4856
|
+
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4857
|
+
}
|
|
4858
|
+
)
|
|
4859
|
+
]
|
|
4860
|
+
},
|
|
4861
|
+
btn
|
|
4862
|
+
)) })
|
|
4863
|
+
] }, group.label))
|
|
4864
|
+
] })
|
|
4865
|
+
]
|
|
4866
|
+
}
|
|
4867
|
+
);
|
|
4816
4868
|
}
|
|
4817
4869
|
function CheatModal({
|
|
4818
4870
|
isOpen,
|
|
@@ -4823,39 +4875,21 @@ function CheatModal({
|
|
|
4823
4875
|
}) {
|
|
4824
4876
|
const t = useKoinTranslation();
|
|
4825
4877
|
const [copiedId, setCopiedId] = React2.useState(null);
|
|
4826
|
-
if (!isOpen) return null;
|
|
4827
4878
|
const handleCopy = async (code, id) => {
|
|
4828
4879
|
await navigator.clipboard.writeText(code);
|
|
4829
|
-
setCopiedId(id);
|
|
4830
|
-
setTimeout(() => setCopiedId(null), 2e3);
|
|
4831
|
-
};
|
|
4832
|
-
return /* @__PURE__ */
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
/* @__PURE__ */
|
|
4842
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4843
|
-
/* @__PURE__ */ jsx(Code, { className: "text-purple-400", size: 24 }),
|
|
4844
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
4845
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
|
|
4846
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
|
|
4847
|
-
] })
|
|
4848
|
-
] }),
|
|
4849
|
-
/* @__PURE__ */ jsx(
|
|
4850
|
-
"button",
|
|
4851
|
-
{
|
|
4852
|
-
onClick: onClose,
|
|
4853
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4854
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
4855
|
-
}
|
|
4856
|
-
)
|
|
4857
|
-
] }),
|
|
4858
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4880
|
+
setCopiedId(id);
|
|
4881
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
4882
|
+
};
|
|
4883
|
+
return /* @__PURE__ */ jsx(
|
|
4884
|
+
ModalShell,
|
|
4885
|
+
{
|
|
4886
|
+
isOpen,
|
|
4887
|
+
onClose,
|
|
4888
|
+
title: t.modals.cheats.title,
|
|
4889
|
+
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4890
|
+
icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
|
|
4891
|
+
footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }),
|
|
4892
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4859
4893
|
/* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4860
4894
|
/* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4861
4895
|
/* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4865,18 +4899,18 @@ function CheatModal({
|
|
|
4865
4899
|
"div",
|
|
4866
4900
|
{
|
|
4867
4901
|
className: `
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4902
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
4903
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4904
|
+
`,
|
|
4871
4905
|
onClick: () => onToggle(cheat.id),
|
|
4872
4906
|
children: [
|
|
4873
4907
|
/* @__PURE__ */ jsx(
|
|
4874
4908
|
"div",
|
|
4875
4909
|
{
|
|
4876
4910
|
className: `
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4911
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4912
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4913
|
+
`,
|
|
4880
4914
|
children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
|
|
4881
4915
|
}
|
|
4882
4916
|
),
|
|
@@ -4902,10 +4936,9 @@ function CheatModal({
|
|
|
4902
4936
|
},
|
|
4903
4937
|
cheat.id
|
|
4904
4938
|
);
|
|
4905
|
-
}) })
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
] });
|
|
4939
|
+
}) })
|
|
4940
|
+
}
|
|
4941
|
+
);
|
|
4909
4942
|
}
|
|
4910
4943
|
var AUTO_SAVE_SLOT = 5;
|
|
4911
4944
|
function formatBytes(bytes) {
|
|
@@ -4946,7 +4979,6 @@ function SaveSlotModal({
|
|
|
4946
4979
|
onUpgrade
|
|
4947
4980
|
}) {
|
|
4948
4981
|
const t = useKoinTranslation();
|
|
4949
|
-
if (!isOpen) return null;
|
|
4950
4982
|
const isSaveMode = mode === "save";
|
|
4951
4983
|
const allSlots = [1, 2, 3, 4, 5];
|
|
4952
4984
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -4961,33 +4993,17 @@ function SaveSlotModal({
|
|
|
4961
4993
|
const getSlotData = (slotNum) => {
|
|
4962
4994
|
return slots.find((s) => s.slot === slotNum);
|
|
4963
4995
|
};
|
|
4964
|
-
return /* @__PURE__ */
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
/* @__PURE__ */
|
|
4974
|
-
|
|
4975
|
-
isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
|
|
4976
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
4977
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
|
|
4978
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
|
|
4979
|
-
] })
|
|
4980
|
-
] }),
|
|
4981
|
-
/* @__PURE__ */ jsx(
|
|
4982
|
-
"button",
|
|
4983
|
-
{
|
|
4984
|
-
onClick: onClose,
|
|
4985
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4986
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
4987
|
-
}
|
|
4988
|
-
)
|
|
4989
|
-
] }),
|
|
4990
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
4996
|
+
return /* @__PURE__ */ jsx(
|
|
4997
|
+
ModalShell,
|
|
4998
|
+
{
|
|
4999
|
+
isOpen,
|
|
5000
|
+
onClose,
|
|
5001
|
+
title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
|
|
5002
|
+
subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
|
|
5003
|
+
icon: isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
|
|
5004
|
+
maxWidth: "md",
|
|
5005
|
+
footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
|
|
5006
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
4991
5007
|
/* @__PURE__ */ jsx(Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
4992
5008
|
/* @__PURE__ */ jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
4993
5009
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5106,10 +5122,9 @@ function SaveSlotModal({
|
|
|
5106
5122
|
},
|
|
5107
5123
|
slotNum
|
|
5108
5124
|
);
|
|
5109
|
-
}) })
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
] });
|
|
5125
|
+
}) })
|
|
5126
|
+
}
|
|
5127
|
+
);
|
|
5113
5128
|
}
|
|
5114
5129
|
function BiosSelectionModal({
|
|
5115
5130
|
isOpen,
|
|
@@ -5256,36 +5271,28 @@ function SettingsModal({
|
|
|
5256
5271
|
systemColor = "#00FF41"
|
|
5257
5272
|
}) {
|
|
5258
5273
|
const t = useKoinTranslation();
|
|
5259
|
-
if (!isOpen) return null;
|
|
5260
5274
|
const languages = [
|
|
5261
5275
|
{ code: "en", name: "English" },
|
|
5262
5276
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5263
5277
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5264
5278
|
];
|
|
5265
|
-
return /* @__PURE__ */
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
}
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
onClick: onClose,
|
|
5283
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5284
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
5285
|
-
}
|
|
5286
|
-
)
|
|
5287
|
-
] }),
|
|
5288
|
-
/* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5279
|
+
return /* @__PURE__ */ jsx(
|
|
5280
|
+
ModalShell,
|
|
5281
|
+
{
|
|
5282
|
+
isOpen,
|
|
5283
|
+
onClose,
|
|
5284
|
+
title: t.settings.title,
|
|
5285
|
+
icon: /* @__PURE__ */ jsx(Settings, { size: 20, className: "text-white" }),
|
|
5286
|
+
maxWidth: "sm",
|
|
5287
|
+
footer: /* @__PURE__ */ jsx(
|
|
5288
|
+
"button",
|
|
5289
|
+
{
|
|
5290
|
+
onClick: onClose,
|
|
5291
|
+
className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
|
|
5292
|
+
children: t.modals.shortcuts.pressEsc
|
|
5293
|
+
}
|
|
5294
|
+
),
|
|
5295
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5289
5296
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5290
5297
|
/* @__PURE__ */ jsx(Globe, { size: 16 }),
|
|
5291
5298
|
/* @__PURE__ */ jsx("span", { children: t.settings.language })
|
|
@@ -5297,9 +5304,9 @@ function SettingsModal({
|
|
|
5297
5304
|
{
|
|
5298
5305
|
onClick: () => onLanguageChange(lang.code),
|
|
5299
5306
|
className: `
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5307
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5308
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5309
|
+
`,
|
|
5303
5310
|
children: [
|
|
5304
5311
|
/* @__PURE__ */ jsx("span", { children: lang.name }),
|
|
5305
5312
|
isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5308,17 +5315,9 @@ function SettingsModal({
|
|
|
5308
5315
|
lang.code
|
|
5309
5316
|
);
|
|
5310
5317
|
}) })
|
|
5311
|
-
] }) })
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
{
|
|
5315
|
-
onClick: onClose,
|
|
5316
|
-
className: "text-sm text-gray-500 hover:text-white transition-colors",
|
|
5317
|
-
children: t.modals.shortcuts.pressEsc
|
|
5318
|
-
}
|
|
5319
|
-
) })
|
|
5320
|
-
] })
|
|
5321
|
-
] });
|
|
5318
|
+
] }) })
|
|
5319
|
+
}
|
|
5320
|
+
);
|
|
5322
5321
|
}
|
|
5323
5322
|
function GameModals({
|
|
5324
5323
|
controlsModalOpen,
|
|
@@ -7529,6 +7528,171 @@ var useNostalgist = ({
|
|
|
7529
7528
|
]);
|
|
7530
7529
|
return hookReturn;
|
|
7531
7530
|
};
|
|
7531
|
+
function getDisplayName(id) {
|
|
7532
|
+
let name = id;
|
|
7533
|
+
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
7534
|
+
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
7535
|
+
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
7536
|
+
if (/xbox/i.test(name)) {
|
|
7537
|
+
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
7538
|
+
if (/one/i.test(name)) return "Xbox One Controller";
|
|
7539
|
+
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
7540
|
+
return "Xbox Controller";
|
|
7541
|
+
}
|
|
7542
|
+
if (/dualsense/i.test(name)) return "DualSense";
|
|
7543
|
+
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
7544
|
+
if (/dualshock/i.test(name)) return "DualShock";
|
|
7545
|
+
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
7546
|
+
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
7547
|
+
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
7548
|
+
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
7549
|
+
return name.trim() || "Gamepad";
|
|
7550
|
+
}
|
|
7551
|
+
function detectControllerBrand(id) {
|
|
7552
|
+
const lowerId = id.toLowerCase();
|
|
7553
|
+
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
7554
|
+
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
7555
|
+
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
7556
|
+
return "generic";
|
|
7557
|
+
}
|
|
7558
|
+
function toGamepadInfo(gamepad) {
|
|
7559
|
+
return {
|
|
7560
|
+
index: gamepad.index,
|
|
7561
|
+
id: gamepad.id,
|
|
7562
|
+
name: getDisplayName(gamepad.id),
|
|
7563
|
+
connected: gamepad.connected,
|
|
7564
|
+
buttons: gamepad.buttons.length,
|
|
7565
|
+
axes: gamepad.axes.length,
|
|
7566
|
+
mapping: gamepad.mapping
|
|
7567
|
+
};
|
|
7568
|
+
}
|
|
7569
|
+
function useGamepad(options) {
|
|
7570
|
+
const { onConnect, onDisconnect } = options || {};
|
|
7571
|
+
const [gamepads, setGamepads] = useState([]);
|
|
7572
|
+
const rafRef = useRef(null);
|
|
7573
|
+
const lastStateRef = useRef("");
|
|
7574
|
+
const prevCountRef = useRef(0);
|
|
7575
|
+
const onConnectRef = useRef(onConnect);
|
|
7576
|
+
const onDisconnectRef = useRef(onDisconnect);
|
|
7577
|
+
useEffect(() => {
|
|
7578
|
+
onConnectRef.current = onConnect;
|
|
7579
|
+
onDisconnectRef.current = onDisconnect;
|
|
7580
|
+
}, [onConnect, onDisconnect]);
|
|
7581
|
+
const getGamepads = useCallback(() => {
|
|
7582
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
7583
|
+
return [];
|
|
7584
|
+
}
|
|
7585
|
+
const rawGamepads = navigator.getGamepads() ?? [];
|
|
7586
|
+
const connected = [];
|
|
7587
|
+
for (let i = 0; i < rawGamepads.length; i++) {
|
|
7588
|
+
const gp = rawGamepads[i];
|
|
7589
|
+
if (gp && gp.connected) {
|
|
7590
|
+
connected.push(toGamepadInfo(gp));
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
return connected;
|
|
7594
|
+
}, []);
|
|
7595
|
+
const getRawGamepad = useCallback((index) => {
|
|
7596
|
+
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
7597
|
+
return rawGamepads[index] ?? null;
|
|
7598
|
+
}, []);
|
|
7599
|
+
const refresh = useCallback(() => {
|
|
7600
|
+
setGamepads(getGamepads());
|
|
7601
|
+
}, [getGamepads]);
|
|
7602
|
+
useEffect(() => {
|
|
7603
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
7604
|
+
return;
|
|
7605
|
+
}
|
|
7606
|
+
if (typeof navigator.getGamepads !== "function") {
|
|
7607
|
+
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
let isActive = true;
|
|
7611
|
+
const poll = () => {
|
|
7612
|
+
if (!isActive) return;
|
|
7613
|
+
const current = getGamepads();
|
|
7614
|
+
let hasChanged = current.length !== prevCountRef.current;
|
|
7615
|
+
if (!hasChanged) {
|
|
7616
|
+
for (let i = 0; i < current.length; i++) {
|
|
7617
|
+
const saved = gamepads[i];
|
|
7618
|
+
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
7619
|
+
hasChanged = true;
|
|
7620
|
+
break;
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
if (hasChanged) {
|
|
7625
|
+
const prevCount = prevCountRef.current;
|
|
7626
|
+
const currentCount = current.length;
|
|
7627
|
+
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
7628
|
+
const newGamepad = current[current.length - 1];
|
|
7629
|
+
onConnectRef.current(newGamepad);
|
|
7630
|
+
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
7631
|
+
onDisconnectRef.current();
|
|
7632
|
+
}
|
|
7633
|
+
prevCountRef.current = currentCount;
|
|
7634
|
+
setGamepads(current);
|
|
7635
|
+
}
|
|
7636
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7637
|
+
};
|
|
7638
|
+
const handleConnect = (e) => {
|
|
7639
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
7640
|
+
const current = getGamepads();
|
|
7641
|
+
const prevCount = prevCountRef.current;
|
|
7642
|
+
prevCountRef.current = current.length;
|
|
7643
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7644
|
+
setGamepads(current);
|
|
7645
|
+
if (onConnectRef.current && current.length > prevCount) {
|
|
7646
|
+
const newGamepad = current[current.length - 1];
|
|
7647
|
+
onConnectRef.current(newGamepad);
|
|
7648
|
+
}
|
|
7649
|
+
};
|
|
7650
|
+
const handleDisconnect = (e) => {
|
|
7651
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
7652
|
+
const current = getGamepads();
|
|
7653
|
+
const prevCount = prevCountRef.current;
|
|
7654
|
+
prevCountRef.current = current.length;
|
|
7655
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7656
|
+
setGamepads(current);
|
|
7657
|
+
if (onDisconnectRef.current && current.length < prevCount) {
|
|
7658
|
+
onDisconnectRef.current();
|
|
7659
|
+
}
|
|
7660
|
+
};
|
|
7661
|
+
window.addEventListener("gamepadconnected", handleConnect);
|
|
7662
|
+
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
7663
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7664
|
+
const initial = getGamepads();
|
|
7665
|
+
if (initial.length > 0) {
|
|
7666
|
+
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
7667
|
+
prevCountRef.current = initial.length;
|
|
7668
|
+
setGamepads(initial);
|
|
7669
|
+
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7670
|
+
} else {
|
|
7671
|
+
prevCountRef.current = 0;
|
|
7672
|
+
}
|
|
7673
|
+
return () => {
|
|
7674
|
+
isActive = false;
|
|
7675
|
+
if (rafRef.current) {
|
|
7676
|
+
cancelAnimationFrame(rafRef.current);
|
|
7677
|
+
}
|
|
7678
|
+
window.removeEventListener("gamepadconnected", handleConnect);
|
|
7679
|
+
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
7680
|
+
};
|
|
7681
|
+
}, [getGamepads]);
|
|
7682
|
+
return {
|
|
7683
|
+
gamepads,
|
|
7684
|
+
isAnyConnected: gamepads.length > 0,
|
|
7685
|
+
connectedCount: gamepads.length,
|
|
7686
|
+
getRawGamepad,
|
|
7687
|
+
refresh
|
|
7688
|
+
};
|
|
7689
|
+
}
|
|
7690
|
+
var STANDARD_AXIS_MAP = {
|
|
7691
|
+
leftStickX: 0,
|
|
7692
|
+
leftStickY: 1,
|
|
7693
|
+
rightStickX: 2,
|
|
7694
|
+
rightStickY: 3
|
|
7695
|
+
};
|
|
7532
7696
|
function useVolume({
|
|
7533
7697
|
setVolume: setVolumeInHook,
|
|
7534
7698
|
toggleMute: toggleMuteInHook
|
|
@@ -9200,6 +9364,14 @@ var GamePlayerInner = memo(function GamePlayerInner2(props) {
|
|
|
9200
9364
|
disabled: status === "loading" || status === "error"
|
|
9201
9365
|
}
|
|
9202
9366
|
),
|
|
9367
|
+
isFullscreen2 && isMobile && (status === "running" || status === "paused") && /* @__PURE__ */ jsx(
|
|
9368
|
+
FloatingPauseButton,
|
|
9369
|
+
{
|
|
9370
|
+
isPaused,
|
|
9371
|
+
onClick: handlePauseToggle,
|
|
9372
|
+
systemColor
|
|
9373
|
+
}
|
|
9374
|
+
),
|
|
9203
9375
|
/* @__PURE__ */ jsxs("div", { className: "absolute top-2 right-2 z-40 flex flex-col items-end gap-2 pointer-events-auto", children: [
|
|
9204
9376
|
/* @__PURE__ */ jsx(
|
|
9205
9377
|
RecordingIndicator_default,
|
|
@@ -9408,27 +9580,15 @@ function AchievementPopup({
|
|
|
9408
9580
|
onDismiss,
|
|
9409
9581
|
autoDismissMs = 5e3
|
|
9410
9582
|
}) {
|
|
9411
|
-
const
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
});
|
|
9417
|
-
const timer = setTimeout(() => {
|
|
9418
|
-
handleDismiss();
|
|
9419
|
-
}, autoDismissMs);
|
|
9420
|
-
return () => clearTimeout(timer);
|
|
9421
|
-
}, [autoDismissMs]);
|
|
9422
|
-
const handleDismiss = () => {
|
|
9423
|
-
setIsExiting(true);
|
|
9424
|
-
setTimeout(() => {
|
|
9425
|
-
onDismiss();
|
|
9426
|
-
}, 300);
|
|
9427
|
-
};
|
|
9583
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9584
|
+
exitDuration: 300,
|
|
9585
|
+
onExit: onDismiss,
|
|
9586
|
+
autoDismissMs
|
|
9587
|
+
});
|
|
9428
9588
|
return /* @__PURE__ */ jsxs(
|
|
9429
9589
|
"div",
|
|
9430
9590
|
{
|
|
9431
|
-
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${
|
|
9591
|
+
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
|
|
9432
9592
|
children: [
|
|
9433
9593
|
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
|
|
9434
9594
|
/* @__PURE__ */ jsx("div", { className: "relative bg-gradient-to-r from-yellow-500 to-orange-500 p-[2px] rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "bg-gray-900 rounded-lg p-4 flex items-center gap-4 min-w-[320px]", children: [
|
|
@@ -9465,7 +9625,7 @@ function AchievementPopup({
|
|
|
9465
9625
|
/* @__PURE__ */ jsx(
|
|
9466
9626
|
"button",
|
|
9467
9627
|
{
|
|
9468
|
-
onClick:
|
|
9628
|
+
onClick: triggerExit,
|
|
9469
9629
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9470
9630
|
children: /* @__PURE__ */ jsx(X, { size: 18 })
|
|
9471
9631
|
}
|