koin.js 1.0.14 → 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 +677 -640
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +678 -641
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1447,6 +1447,40 @@ var PlayerControls = React2.memo(function PlayerControls2({
|
|
|
1447
1447
|
] });
|
|
1448
1448
|
});
|
|
1449
1449
|
var PlayerControls_default = PlayerControls;
|
|
1450
|
+
function useAnimatedVisibility({
|
|
1451
|
+
exitDuration = 200,
|
|
1452
|
+
onExit,
|
|
1453
|
+
autoDismissMs
|
|
1454
|
+
} = {}) {
|
|
1455
|
+
const [isVisible, setIsVisible] = React2.useState(false);
|
|
1456
|
+
const [isExiting, setIsExiting] = React2.useState(false);
|
|
1457
|
+
React2.useEffect(() => {
|
|
1458
|
+
requestAnimationFrame(() => {
|
|
1459
|
+
setIsVisible(true);
|
|
1460
|
+
});
|
|
1461
|
+
}, []);
|
|
1462
|
+
React2.useEffect(() => {
|
|
1463
|
+
if (!autoDismissMs) return;
|
|
1464
|
+
const timer = setTimeout(() => {
|
|
1465
|
+
triggerExit();
|
|
1466
|
+
}, autoDismissMs);
|
|
1467
|
+
return () => clearTimeout(timer);
|
|
1468
|
+
}, [autoDismissMs]);
|
|
1469
|
+
const triggerExit = React2.useCallback(() => {
|
|
1470
|
+
if (isExiting) return;
|
|
1471
|
+
setIsExiting(true);
|
|
1472
|
+
setTimeout(() => {
|
|
1473
|
+
onExit?.();
|
|
1474
|
+
}, exitDuration);
|
|
1475
|
+
}, [isExiting, exitDuration, onExit]);
|
|
1476
|
+
const slideInRightClasses = isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0";
|
|
1477
|
+
return {
|
|
1478
|
+
isVisible,
|
|
1479
|
+
isExiting,
|
|
1480
|
+
triggerExit,
|
|
1481
|
+
slideInRightClasses
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1450
1484
|
var TOAST_CONFIGS = {
|
|
1451
1485
|
success: {
|
|
1452
1486
|
icon: lucideReact.CheckCircle,
|
|
@@ -1491,32 +1525,20 @@ var TOAST_CONFIGS = {
|
|
|
1491
1525
|
}
|
|
1492
1526
|
};
|
|
1493
1527
|
function ToastItem({ toast, onDismiss }) {
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1528
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
1529
|
+
exitDuration: 200,
|
|
1530
|
+
onExit: () => onDismiss?.(toast.id)
|
|
1531
|
+
});
|
|
1496
1532
|
const config = TOAST_CONFIGS[toast.type];
|
|
1497
1533
|
const IconComponent = config.icon;
|
|
1498
|
-
React2.useEffect(() => {
|
|
1499
|
-
requestAnimationFrame(() => {
|
|
1500
|
-
setIsVisible(true);
|
|
1501
|
-
});
|
|
1502
|
-
}, []);
|
|
1503
|
-
const handleDismiss = () => {
|
|
1504
|
-
setIsExiting(true);
|
|
1505
|
-
setTimeout(() => {
|
|
1506
|
-
onDismiss?.(toast.id);
|
|
1507
|
-
}, 200);
|
|
1508
|
-
};
|
|
1509
1534
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1510
1535
|
"div",
|
|
1511
1536
|
{
|
|
1512
|
-
className: `
|
|
1513
|
-
relative transition-all duration-300 ease-out
|
|
1514
|
-
${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
|
|
1515
|
-
`,
|
|
1537
|
+
className: `relative transition-all duration-300 ease-out ${slideInRightClasses}`,
|
|
1516
1538
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1517
1539
|
"div",
|
|
1518
1540
|
{
|
|
1519
|
-
className: "relative w-[320px]",
|
|
1541
|
+
className: "relative w-[320px] pointer-events-auto",
|
|
1520
1542
|
style: {
|
|
1521
1543
|
backgroundColor: config.bgColor,
|
|
1522
1544
|
border: `2px solid ${config.borderColor}`,
|
|
@@ -1551,7 +1573,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1551
1573
|
{
|
|
1552
1574
|
onClick: () => {
|
|
1553
1575
|
toast.action?.onClick();
|
|
1554
|
-
|
|
1576
|
+
triggerExit();
|
|
1555
1577
|
},
|
|
1556
1578
|
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",
|
|
1557
1579
|
style: {
|
|
@@ -1565,7 +1587,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1565
1587
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1566
1588
|
"button",
|
|
1567
1589
|
{
|
|
1568
|
-
onClick:
|
|
1590
|
+
onClick: triggerExit,
|
|
1569
1591
|
className: "flex-shrink-0 p-0.5 text-gray-500 hover:text-white transition-colors",
|
|
1570
1592
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14 })
|
|
1571
1593
|
}
|
|
@@ -1938,6 +1960,67 @@ var RecordingIndicator = React2.memo(function RecordingIndicator2({
|
|
|
1938
1960
|
);
|
|
1939
1961
|
});
|
|
1940
1962
|
var RecordingIndicator_default = RecordingIndicator;
|
|
1963
|
+
var MAX_WIDTH_CLASSES = {
|
|
1964
|
+
sm: "max-w-sm",
|
|
1965
|
+
md: "max-w-md",
|
|
1966
|
+
lg: "max-w-lg"
|
|
1967
|
+
};
|
|
1968
|
+
function ModalShell({
|
|
1969
|
+
isOpen,
|
|
1970
|
+
onClose,
|
|
1971
|
+
title,
|
|
1972
|
+
subtitle,
|
|
1973
|
+
icon,
|
|
1974
|
+
children,
|
|
1975
|
+
footer,
|
|
1976
|
+
maxWidth = "lg",
|
|
1977
|
+
systemColor,
|
|
1978
|
+
closeOnBackdrop = true
|
|
1979
|
+
}) {
|
|
1980
|
+
if (!isOpen) return null;
|
|
1981
|
+
const handleBackdropClick = () => {
|
|
1982
|
+
if (closeOnBackdrop) {
|
|
1983
|
+
onClose();
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1988
|
+
"div",
|
|
1989
|
+
{
|
|
1990
|
+
className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
|
|
1991
|
+
onClick: handleBackdropClick
|
|
1992
|
+
}
|
|
1993
|
+
),
|
|
1994
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1995
|
+
"div",
|
|
1996
|
+
{
|
|
1997
|
+
className: `relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full ${MAX_WIDTH_CLASSES[maxWidth]} mx-4 overflow-hidden`,
|
|
1998
|
+
style: systemColor ? { borderColor: `${systemColor}30` } : void 0,
|
|
1999
|
+
children: [
|
|
2000
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
|
|
2001
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: systemColor ? { color: systemColor } : void 0, children: icon }),
|
|
2003
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2004
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: title }),
|
|
2005
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: subtitle })
|
|
2006
|
+
] })
|
|
2007
|
+
] }),
|
|
2008
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2009
|
+
"button",
|
|
2010
|
+
{
|
|
2011
|
+
onClick: onClose,
|
|
2012
|
+
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
2013
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
2014
|
+
}
|
|
2015
|
+
)
|
|
2016
|
+
] }),
|
|
2017
|
+
children,
|
|
2018
|
+
footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: footer })
|
|
2019
|
+
]
|
|
2020
|
+
}
|
|
2021
|
+
)
|
|
2022
|
+
] });
|
|
2023
|
+
}
|
|
1941
2024
|
var ShortcutsModal = React2.memo(function ShortcutsModal2({
|
|
1942
2025
|
isOpen,
|
|
1943
2026
|
onClose,
|
|
@@ -1966,90 +2049,55 @@ var ShortcutsModal = React2.memo(function ShortcutsModal2({
|
|
|
1966
2049
|
]
|
|
1967
2050
|
}
|
|
1968
2051
|
], [t]);
|
|
1969
|
-
if (!isOpen) return null;
|
|
1970
2052
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1971
|
-
|
|
2053
|
+
ModalShell,
|
|
1972
2054
|
{
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
className: "p-1 rounded hover:bg-white/10 transition-colors",
|
|
1997
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18, className: "text-white/60 hover:text-white" })
|
|
1998
|
-
}
|
|
1999
|
-
)
|
|
2000
|
-
]
|
|
2001
|
-
}
|
|
2002
|
-
),
|
|
2003
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
|
|
2004
|
-
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2055
|
+
isOpen,
|
|
2056
|
+
onClose,
|
|
2057
|
+
title: t.modals.shortcuts.playerShortcuts,
|
|
2058
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Keyboard, { size: 20, style: { color: systemColor } }),
|
|
2059
|
+
maxWidth: "sm",
|
|
2060
|
+
systemColor,
|
|
2061
|
+
footer: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-500 w-full text-center", children: t.modals.shortcuts.pressEsc }),
|
|
2062
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
|
|
2063
|
+
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2064
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2065
|
+
"h3",
|
|
2066
|
+
{
|
|
2067
|
+
className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
|
|
2068
|
+
style: { color: systemColor },
|
|
2069
|
+
children: section
|
|
2070
|
+
}
|
|
2071
|
+
),
|
|
2072
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2073
|
+
"div",
|
|
2074
|
+
{
|
|
2075
|
+
className: "flex items-center justify-between text-sm",
|
|
2076
|
+
children: [
|
|
2077
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/70", children: description }),
|
|
2005
2078
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2006
|
-
"
|
|
2079
|
+
"kbd",
|
|
2007
2080
|
{
|
|
2008
|
-
className: "
|
|
2009
|
-
style: {
|
|
2010
|
-
|
|
2081
|
+
className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
|
|
2082
|
+
style: {
|
|
2083
|
+
backgroundColor: `${systemColor}20`,
|
|
2084
|
+
color: systemColor,
|
|
2085
|
+
border: `1px solid ${systemColor}40`
|
|
2086
|
+
},
|
|
2087
|
+
children: key
|
|
2011
2088
|
}
|
|
2012
|
-
)
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
backgroundColor: `${systemColor}20`,
|
|
2025
|
-
color: systemColor,
|
|
2026
|
-
border: `1px solid ${systemColor}40`
|
|
2027
|
-
},
|
|
2028
|
-
children: key
|
|
2029
|
-
}
|
|
2030
|
-
)
|
|
2031
|
-
]
|
|
2032
|
-
},
|
|
2033
|
-
key
|
|
2034
|
-
)) })
|
|
2035
|
-
] }, section)),
|
|
2036
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2037
|
-
"Game controls can be configured in ",
|
|
2038
|
-
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2039
|
-
" settings."
|
|
2040
|
-
] })
|
|
2041
|
-
] }),
|
|
2042
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2043
|
-
"div",
|
|
2044
|
-
{
|
|
2045
|
-
className: "px-4 py-2 text-center text-xs text-white/40 border-t",
|
|
2046
|
-
style: { borderColor: `${systemColor}20` },
|
|
2047
|
-
children: t.modals.shortcuts.pressEsc
|
|
2048
|
-
}
|
|
2049
|
-
)
|
|
2050
|
-
]
|
|
2051
|
-
}
|
|
2052
|
-
)
|
|
2089
|
+
)
|
|
2090
|
+
]
|
|
2091
|
+
},
|
|
2092
|
+
key
|
|
2093
|
+
)) })
|
|
2094
|
+
] }, section)),
|
|
2095
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2096
|
+
"Game controls can be configured in ",
|
|
2097
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2098
|
+
" settings."
|
|
2099
|
+
] })
|
|
2100
|
+
] })
|
|
2053
2101
|
}
|
|
2054
2102
|
);
|
|
2055
2103
|
});
|
|
@@ -2646,6 +2694,39 @@ function useTouchHandlers({
|
|
|
2646
2694
|
cleanup
|
|
2647
2695
|
};
|
|
2648
2696
|
}
|
|
2697
|
+
function useTouchEvents(ref, handlers, options = {}) {
|
|
2698
|
+
const { cleanup, passive = false } = options;
|
|
2699
|
+
const handlersRef = React2.useRef(handlers);
|
|
2700
|
+
handlersRef.current = handlers;
|
|
2701
|
+
const handleTouchStart = React2.useCallback((e) => {
|
|
2702
|
+
handlersRef.current.onTouchStart?.(e);
|
|
2703
|
+
}, []);
|
|
2704
|
+
const handleTouchMove = React2.useCallback((e) => {
|
|
2705
|
+
handlersRef.current.onTouchMove?.(e);
|
|
2706
|
+
}, []);
|
|
2707
|
+
const handleTouchEnd = React2.useCallback((e) => {
|
|
2708
|
+
handlersRef.current.onTouchEnd?.(e);
|
|
2709
|
+
}, []);
|
|
2710
|
+
const handleTouchCancel = React2.useCallback((e) => {
|
|
2711
|
+
handlersRef.current.onTouchCancel?.(e);
|
|
2712
|
+
}, []);
|
|
2713
|
+
React2.useEffect(() => {
|
|
2714
|
+
const element = ref.current;
|
|
2715
|
+
if (!element) return;
|
|
2716
|
+
const listenerOptions = { passive };
|
|
2717
|
+
element.addEventListener("touchstart", handleTouchStart, listenerOptions);
|
|
2718
|
+
element.addEventListener("touchmove", handleTouchMove, listenerOptions);
|
|
2719
|
+
element.addEventListener("touchend", handleTouchEnd, listenerOptions);
|
|
2720
|
+
element.addEventListener("touchcancel", handleTouchCancel, listenerOptions);
|
|
2721
|
+
return () => {
|
|
2722
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
2723
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
2724
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
2725
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
2726
|
+
cleanup?.();
|
|
2727
|
+
};
|
|
2728
|
+
}, [ref, passive, cleanup, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
2729
|
+
}
|
|
2649
2730
|
|
|
2650
2731
|
// src/components/VirtualController/utils/buttonStyles.ts
|
|
2651
2732
|
var DEFAULT_FACE = {
|
|
@@ -2803,21 +2884,12 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2803
2884
|
onRelease,
|
|
2804
2885
|
onPositionChange
|
|
2805
2886
|
});
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
|
|
2813
|
-
return () => {
|
|
2814
|
-
button.removeEventListener("touchstart", handleTouchStart);
|
|
2815
|
-
button.removeEventListener("touchmove", handleTouchMove);
|
|
2816
|
-
button.removeEventListener("touchend", handleTouchEnd);
|
|
2817
|
-
button.removeEventListener("touchcancel", handleTouchCancel);
|
|
2818
|
-
cleanup();
|
|
2819
|
-
};
|
|
2820
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
|
|
2887
|
+
useTouchEvents(buttonRef, {
|
|
2888
|
+
onTouchStart: handleTouchStart,
|
|
2889
|
+
onTouchMove: handleTouchMove,
|
|
2890
|
+
onTouchEnd: handleTouchEnd,
|
|
2891
|
+
onTouchCancel: handleTouchCancel
|
|
2892
|
+
}, { cleanup });
|
|
2821
2893
|
const leftPercent = displayX / 100 * containerWidth - config.size / 2;
|
|
2822
2894
|
const topPercent = displayY / 100 * containerHeight - config.size / 2;
|
|
2823
2895
|
const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
|
|
@@ -3493,7 +3565,7 @@ function dispatchKeyboardEvent(type, code) {
|
|
|
3493
3565
|
canvas.dispatchEvent(event);
|
|
3494
3566
|
return true;
|
|
3495
3567
|
}
|
|
3496
|
-
var CENTER_TOUCH_RADIUS = 0.
|
|
3568
|
+
var CENTER_TOUCH_RADIUS = 0.35;
|
|
3497
3569
|
var Dpad = React2__default.default.memo(function Dpad2({
|
|
3498
3570
|
size = 180,
|
|
3499
3571
|
x,
|
|
@@ -3667,21 +3739,12 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3667
3739
|
}
|
|
3668
3740
|
}
|
|
3669
3741
|
}, [getKeyCode, updateVisuals, drag]);
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
|
|
3677
|
-
return () => {
|
|
3678
|
-
dpad.removeEventListener("touchstart", handleTouchStart);
|
|
3679
|
-
dpad.removeEventListener("touchmove", handleTouchMove);
|
|
3680
|
-
dpad.removeEventListener("touchend", handleTouchEnd);
|
|
3681
|
-
dpad.removeEventListener("touchcancel", handleTouchEnd);
|
|
3682
|
-
drag.clearDragTimer();
|
|
3683
|
-
};
|
|
3684
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
|
|
3742
|
+
useTouchEvents(dpadRef, {
|
|
3743
|
+
onTouchStart: handleTouchStart,
|
|
3744
|
+
onTouchMove: handleTouchMove,
|
|
3745
|
+
onTouchEnd: handleTouchEnd,
|
|
3746
|
+
onTouchCancel: handleTouchEnd
|
|
3747
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3685
3748
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3686
3749
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3687
3750
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3881,6 +3944,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3881
3944
|
}
|
|
3882
3945
|
);
|
|
3883
3946
|
}
|
|
3947
|
+
function LockButton({
|
|
3948
|
+
isLocked,
|
|
3949
|
+
onToggle,
|
|
3950
|
+
systemColor = "#00FF41"
|
|
3951
|
+
}) {
|
|
3952
|
+
const Icon = isLocked ? lucideReact.Lock : lucideReact.Unlock;
|
|
3953
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3954
|
+
"button",
|
|
3955
|
+
{
|
|
3956
|
+
onClick: onToggle,
|
|
3957
|
+
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",
|
|
3958
|
+
style: {
|
|
3959
|
+
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3960
|
+
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
3961
|
+
},
|
|
3962
|
+
"aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
|
|
3963
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3964
|
+
Icon,
|
|
3965
|
+
{
|
|
3966
|
+
size: 18,
|
|
3967
|
+
style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
|
|
3968
|
+
}
|
|
3969
|
+
)
|
|
3970
|
+
}
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
var LOCK_KEY = "koin-controls-locked";
|
|
3884
3974
|
function VirtualController({
|
|
3885
3975
|
system,
|
|
3886
3976
|
isRunning,
|
|
@@ -3892,7 +3982,21 @@ function VirtualController({
|
|
|
3892
3982
|
const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
3893
3983
|
const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
|
|
3894
3984
|
const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
|
|
3985
|
+
const [isLocked, setIsLocked] = React2.useState(true);
|
|
3895
3986
|
const { getPosition, savePosition } = useButtonPositions();
|
|
3987
|
+
React2.useEffect(() => {
|
|
3988
|
+
const stored = localStorage.getItem(LOCK_KEY);
|
|
3989
|
+
if (stored !== null) {
|
|
3990
|
+
setIsLocked(stored === "true");
|
|
3991
|
+
}
|
|
3992
|
+
}, []);
|
|
3993
|
+
const toggleLock = React2.useCallback(() => {
|
|
3994
|
+
setIsLocked((prev) => {
|
|
3995
|
+
const newValue = !prev;
|
|
3996
|
+
localStorage.setItem(LOCK_KEY, String(newValue));
|
|
3997
|
+
return newValue;
|
|
3998
|
+
});
|
|
3999
|
+
}, []);
|
|
3896
4000
|
const layout = getLayoutForSystem(system);
|
|
3897
4001
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3898
4002
|
if (isPortrait) {
|
|
@@ -4047,6 +4151,14 @@ function VirtualController({
|
|
|
4047
4151
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
4048
4152
|
style: { touchAction: "none" },
|
|
4049
4153
|
children: [
|
|
4154
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4155
|
+
LockButton,
|
|
4156
|
+
{
|
|
4157
|
+
isLocked,
|
|
4158
|
+
onToggle: toggleLock,
|
|
4159
|
+
systemColor
|
|
4160
|
+
}
|
|
4161
|
+
),
|
|
4050
4162
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4051
4163
|
Dpad_default,
|
|
4052
4164
|
{
|
|
@@ -4059,7 +4171,7 @@ function VirtualController({
|
|
|
4059
4171
|
systemColor,
|
|
4060
4172
|
isLandscape,
|
|
4061
4173
|
customPosition: getPosition("up", isLandscape),
|
|
4062
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4174
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
4063
4175
|
}
|
|
4064
4176
|
),
|
|
4065
4177
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4073,7 +4185,7 @@ function VirtualController({
|
|
|
4073
4185
|
containerWidth: width,
|
|
4074
4186
|
containerHeight: height,
|
|
4075
4187
|
customPosition,
|
|
4076
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4188
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4077
4189
|
isLandscape,
|
|
4078
4190
|
console: layout.console
|
|
4079
4191
|
},
|
|
@@ -4393,6 +4505,45 @@ var GameCanvas = React2.memo(function GameCanvas2({
|
|
|
4393
4505
|
] });
|
|
4394
4506
|
});
|
|
4395
4507
|
var GameCanvas_default = GameCanvas;
|
|
4508
|
+
function useInputCapture({
|
|
4509
|
+
isOpen,
|
|
4510
|
+
onClose
|
|
4511
|
+
}) {
|
|
4512
|
+
const [listeningFor, setListeningFor] = React2.useState(null);
|
|
4513
|
+
const startListening = React2.useCallback((target) => {
|
|
4514
|
+
setListeningFor(target);
|
|
4515
|
+
}, []);
|
|
4516
|
+
const stopListening = React2.useCallback(() => {
|
|
4517
|
+
setListeningFor(null);
|
|
4518
|
+
}, []);
|
|
4519
|
+
React2.useEffect(() => {
|
|
4520
|
+
if (!isOpen) {
|
|
4521
|
+
setListeningFor(null);
|
|
4522
|
+
}
|
|
4523
|
+
}, [isOpen]);
|
|
4524
|
+
React2.useEffect(() => {
|
|
4525
|
+
if (!isOpen) return;
|
|
4526
|
+
const handleKeyDown = (e) => {
|
|
4527
|
+
if (e.code === "Escape") {
|
|
4528
|
+
if (listeningFor !== null) {
|
|
4529
|
+
e.preventDefault();
|
|
4530
|
+
e.stopPropagation();
|
|
4531
|
+
setListeningFor(null);
|
|
4532
|
+
} else {
|
|
4533
|
+
onClose();
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
};
|
|
4537
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4538
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4539
|
+
}, [isOpen, listeningFor, onClose]);
|
|
4540
|
+
return {
|
|
4541
|
+
listeningFor,
|
|
4542
|
+
startListening,
|
|
4543
|
+
stopListening,
|
|
4544
|
+
isListening: listeningFor !== null
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4396
4547
|
function getFilteredGroups(activeButtons) {
|
|
4397
4548
|
return BUTTON_GROUPS.map((group) => ({
|
|
4398
4549
|
...group,
|
|
@@ -4408,7 +4559,10 @@ function ControlMapper({
|
|
|
4408
4559
|
}) {
|
|
4409
4560
|
const t = useKoinTranslation();
|
|
4410
4561
|
const [localControls, setLocalControls] = React2.useState(controls);
|
|
4411
|
-
const
|
|
4562
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4563
|
+
isOpen,
|
|
4564
|
+
onClose
|
|
4565
|
+
});
|
|
4412
4566
|
const activeButtons = React2.useMemo(() => {
|
|
4413
4567
|
return getConsoleButtons(system || "SNES");
|
|
4414
4568
|
}, [system]);
|
|
@@ -4424,27 +4578,20 @@ function ControlMapper({
|
|
|
4424
4578
|
}
|
|
4425
4579
|
}, [isOpen, controls]);
|
|
4426
4580
|
React2.useEffect(() => {
|
|
4427
|
-
if (!isOpen)
|
|
4428
|
-
setListeningFor(null);
|
|
4429
|
-
return;
|
|
4430
|
-
}
|
|
4581
|
+
if (!isOpen || !listeningFor) return;
|
|
4431
4582
|
const handleKeyDown = (e) => {
|
|
4432
|
-
if (
|
|
4583
|
+
if (e.code === "Escape") return;
|
|
4433
4584
|
e.preventDefault();
|
|
4434
4585
|
e.stopPropagation();
|
|
4435
|
-
if (e.code === "Escape") {
|
|
4436
|
-
setListeningFor(null);
|
|
4437
|
-
return;
|
|
4438
|
-
}
|
|
4439
4586
|
setLocalControls((prev) => ({
|
|
4440
4587
|
...prev,
|
|
4441
4588
|
[listeningFor]: e.code
|
|
4442
4589
|
}));
|
|
4443
|
-
|
|
4590
|
+
stopListening();
|
|
4444
4591
|
};
|
|
4445
4592
|
window.addEventListener("keydown", handleKeyDown);
|
|
4446
4593
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4447
|
-
}, [isOpen, listeningFor]);
|
|
4594
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4448
4595
|
const handleReset = () => {
|
|
4449
4596
|
setLocalControls(defaultControls);
|
|
4450
4597
|
};
|
|
@@ -4452,52 +4599,58 @@ function ControlMapper({
|
|
|
4452
4599
|
onSave(localControls);
|
|
4453
4600
|
onClose();
|
|
4454
4601
|
};
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4602
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4603
|
+
ModalShell,
|
|
4604
|
+
{
|
|
4605
|
+
isOpen,
|
|
4606
|
+
onClose,
|
|
4607
|
+
title: t.modals.controls.title,
|
|
4608
|
+
subtitle: t.modals.controls.description,
|
|
4609
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
|
|
4610
|
+
closeOnBackdrop: !isListening,
|
|
4611
|
+
footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4612
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4613
|
+
"button",
|
|
4614
|
+
{
|
|
4615
|
+
onClick: handleReset,
|
|
4616
|
+
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",
|
|
4617
|
+
children: [
|
|
4618
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
|
|
4619
|
+
t.modals.controls.reset
|
|
4620
|
+
]
|
|
4621
|
+
}
|
|
4622
|
+
),
|
|
4623
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4474
4624
|
"button",
|
|
4475
4625
|
{
|
|
4476
|
-
onClick:
|
|
4477
|
-
className: "
|
|
4478
|
-
children:
|
|
4626
|
+
onClick: handleSave,
|
|
4627
|
+
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",
|
|
4628
|
+
children: [
|
|
4629
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
|
|
4630
|
+
t.modals.controls.save
|
|
4631
|
+
]
|
|
4479
4632
|
}
|
|
4480
4633
|
)
|
|
4481
4634
|
] }),
|
|
4482
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4635
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4483
4636
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4484
4637
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4485
4638
|
"button",
|
|
4486
4639
|
{
|
|
4487
|
-
onClick: () =>
|
|
4640
|
+
onClick: () => startListening(btn),
|
|
4488
4641
|
className: `
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4642
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4643
|
+
${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"}
|
|
4644
|
+
`,
|
|
4492
4645
|
children: [
|
|
4493
4646
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4494
4647
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4495
4648
|
"span",
|
|
4496
4649
|
{
|
|
4497
4650
|
className: `
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4651
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4652
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4653
|
+
`,
|
|
4501
4654
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4502
4655
|
}
|
|
4503
4656
|
)
|
|
@@ -4505,199 +4658,10 @@ function ControlMapper({
|
|
|
4505
4658
|
},
|
|
4506
4659
|
btn
|
|
4507
4660
|
)) })
|
|
4508
|
-
] }, group.label)) })
|
|
4509
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4510
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4511
|
-
"button",
|
|
4512
|
-
{
|
|
4513
|
-
onClick: handleReset,
|
|
4514
|
-
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",
|
|
4515
|
-
children: [
|
|
4516
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
|
|
4517
|
-
t.modals.controls.reset
|
|
4518
|
-
]
|
|
4519
|
-
}
|
|
4520
|
-
),
|
|
4521
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4522
|
-
"button",
|
|
4523
|
-
{
|
|
4524
|
-
onClick: handleSave,
|
|
4525
|
-
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",
|
|
4526
|
-
children: [
|
|
4527
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
|
|
4528
|
-
t.modals.controls.save
|
|
4529
|
-
]
|
|
4530
|
-
}
|
|
4531
|
-
)
|
|
4532
|
-
] })
|
|
4533
|
-
] })
|
|
4534
|
-
] });
|
|
4535
|
-
}
|
|
4536
|
-
function getDisplayName(id) {
|
|
4537
|
-
let name = id;
|
|
4538
|
-
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
4539
|
-
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
4540
|
-
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
4541
|
-
if (/xbox/i.test(name)) {
|
|
4542
|
-
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
4543
|
-
if (/one/i.test(name)) return "Xbox One Controller";
|
|
4544
|
-
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
4545
|
-
return "Xbox Controller";
|
|
4546
|
-
}
|
|
4547
|
-
if (/dualsense/i.test(name)) return "DualSense";
|
|
4548
|
-
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
4549
|
-
if (/dualshock/i.test(name)) return "DualShock";
|
|
4550
|
-
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
4551
|
-
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
4552
|
-
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
4553
|
-
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
4554
|
-
return name.trim() || "Gamepad";
|
|
4555
|
-
}
|
|
4556
|
-
function detectControllerBrand(id) {
|
|
4557
|
-
const lowerId = id.toLowerCase();
|
|
4558
|
-
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
4559
|
-
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
4560
|
-
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
4561
|
-
return "generic";
|
|
4562
|
-
}
|
|
4563
|
-
function toGamepadInfo(gamepad) {
|
|
4564
|
-
return {
|
|
4565
|
-
index: gamepad.index,
|
|
4566
|
-
id: gamepad.id,
|
|
4567
|
-
name: getDisplayName(gamepad.id),
|
|
4568
|
-
connected: gamepad.connected,
|
|
4569
|
-
buttons: gamepad.buttons.length,
|
|
4570
|
-
axes: gamepad.axes.length,
|
|
4571
|
-
mapping: gamepad.mapping
|
|
4572
|
-
};
|
|
4573
|
-
}
|
|
4574
|
-
function useGamepad(options) {
|
|
4575
|
-
const { onConnect, onDisconnect } = options || {};
|
|
4576
|
-
const [gamepads, setGamepads] = React2.useState([]);
|
|
4577
|
-
const rafRef = React2.useRef(null);
|
|
4578
|
-
const lastStateRef = React2.useRef("");
|
|
4579
|
-
const prevCountRef = React2.useRef(0);
|
|
4580
|
-
const onConnectRef = React2.useRef(onConnect);
|
|
4581
|
-
const onDisconnectRef = React2.useRef(onDisconnect);
|
|
4582
|
-
React2.useEffect(() => {
|
|
4583
|
-
onConnectRef.current = onConnect;
|
|
4584
|
-
onDisconnectRef.current = onDisconnect;
|
|
4585
|
-
}, [onConnect, onDisconnect]);
|
|
4586
|
-
const getGamepads = React2.useCallback(() => {
|
|
4587
|
-
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
4588
|
-
return [];
|
|
4661
|
+
] }, group.label)) })
|
|
4589
4662
|
}
|
|
4590
|
-
|
|
4591
|
-
const connected = [];
|
|
4592
|
-
for (let i = 0; i < rawGamepads.length; i++) {
|
|
4593
|
-
const gp = rawGamepads[i];
|
|
4594
|
-
if (gp && gp.connected) {
|
|
4595
|
-
connected.push(toGamepadInfo(gp));
|
|
4596
|
-
}
|
|
4597
|
-
}
|
|
4598
|
-
return connected;
|
|
4599
|
-
}, []);
|
|
4600
|
-
const getRawGamepad = React2.useCallback((index) => {
|
|
4601
|
-
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
4602
|
-
return rawGamepads[index] ?? null;
|
|
4603
|
-
}, []);
|
|
4604
|
-
const refresh = React2.useCallback(() => {
|
|
4605
|
-
setGamepads(getGamepads());
|
|
4606
|
-
}, [getGamepads]);
|
|
4607
|
-
React2.useEffect(() => {
|
|
4608
|
-
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
4609
|
-
return;
|
|
4610
|
-
}
|
|
4611
|
-
if (typeof navigator.getGamepads !== "function") {
|
|
4612
|
-
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
4613
|
-
return;
|
|
4614
|
-
}
|
|
4615
|
-
let isActive = true;
|
|
4616
|
-
const poll = () => {
|
|
4617
|
-
if (!isActive) return;
|
|
4618
|
-
const current = getGamepads();
|
|
4619
|
-
let hasChanged = current.length !== prevCountRef.current;
|
|
4620
|
-
if (!hasChanged) {
|
|
4621
|
-
for (let i = 0; i < current.length; i++) {
|
|
4622
|
-
const saved = gamepads[i];
|
|
4623
|
-
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
4624
|
-
hasChanged = true;
|
|
4625
|
-
break;
|
|
4626
|
-
}
|
|
4627
|
-
}
|
|
4628
|
-
}
|
|
4629
|
-
if (hasChanged) {
|
|
4630
|
-
const prevCount = prevCountRef.current;
|
|
4631
|
-
const currentCount = current.length;
|
|
4632
|
-
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
4633
|
-
const newGamepad = current[current.length - 1];
|
|
4634
|
-
onConnectRef.current(newGamepad);
|
|
4635
|
-
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
4636
|
-
onDisconnectRef.current();
|
|
4637
|
-
}
|
|
4638
|
-
prevCountRef.current = currentCount;
|
|
4639
|
-
setGamepads(current);
|
|
4640
|
-
}
|
|
4641
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4642
|
-
};
|
|
4643
|
-
const handleConnect = (e) => {
|
|
4644
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
4645
|
-
const current = getGamepads();
|
|
4646
|
-
const prevCount = prevCountRef.current;
|
|
4647
|
-
prevCountRef.current = current.length;
|
|
4648
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4649
|
-
setGamepads(current);
|
|
4650
|
-
if (onConnectRef.current && current.length > prevCount) {
|
|
4651
|
-
const newGamepad = current[current.length - 1];
|
|
4652
|
-
onConnectRef.current(newGamepad);
|
|
4653
|
-
}
|
|
4654
|
-
};
|
|
4655
|
-
const handleDisconnect = (e) => {
|
|
4656
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
4657
|
-
const current = getGamepads();
|
|
4658
|
-
const prevCount = prevCountRef.current;
|
|
4659
|
-
prevCountRef.current = current.length;
|
|
4660
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4661
|
-
setGamepads(current);
|
|
4662
|
-
if (onDisconnectRef.current && current.length < prevCount) {
|
|
4663
|
-
onDisconnectRef.current();
|
|
4664
|
-
}
|
|
4665
|
-
};
|
|
4666
|
-
window.addEventListener("gamepadconnected", handleConnect);
|
|
4667
|
-
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
4668
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4669
|
-
const initial = getGamepads();
|
|
4670
|
-
if (initial.length > 0) {
|
|
4671
|
-
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
4672
|
-
prevCountRef.current = initial.length;
|
|
4673
|
-
setGamepads(initial);
|
|
4674
|
-
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4675
|
-
} else {
|
|
4676
|
-
prevCountRef.current = 0;
|
|
4677
|
-
}
|
|
4678
|
-
return () => {
|
|
4679
|
-
isActive = false;
|
|
4680
|
-
if (rafRef.current) {
|
|
4681
|
-
cancelAnimationFrame(rafRef.current);
|
|
4682
|
-
}
|
|
4683
|
-
window.removeEventListener("gamepadconnected", handleConnect);
|
|
4684
|
-
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
4685
|
-
};
|
|
4686
|
-
}, [getGamepads]);
|
|
4687
|
-
return {
|
|
4688
|
-
gamepads,
|
|
4689
|
-
isAnyConnected: gamepads.length > 0,
|
|
4690
|
-
connectedCount: gamepads.length,
|
|
4691
|
-
getRawGamepad,
|
|
4692
|
-
refresh
|
|
4693
|
-
};
|
|
4663
|
+
);
|
|
4694
4664
|
}
|
|
4695
|
-
var STANDARD_AXIS_MAP = {
|
|
4696
|
-
leftStickX: 0,
|
|
4697
|
-
leftStickY: 1,
|
|
4698
|
-
rightStickX: 2,
|
|
4699
|
-
rightStickY: 3
|
|
4700
|
-
};
|
|
4701
4665
|
function GamepadMapper({
|
|
4702
4666
|
isOpen,
|
|
4703
4667
|
gamepads,
|
|
@@ -4708,7 +4672,10 @@ function GamepadMapper({
|
|
|
4708
4672
|
const t = useKoinTranslation();
|
|
4709
4673
|
const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
|
|
4710
4674
|
const [bindings, setBindings] = React2.useState({});
|
|
4711
|
-
const
|
|
4675
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4676
|
+
isOpen,
|
|
4677
|
+
onClose
|
|
4678
|
+
});
|
|
4712
4679
|
const rafRef = React2.useRef(null);
|
|
4713
4680
|
React2.useEffect(() => {
|
|
4714
4681
|
if (isOpen) {
|
|
@@ -4743,7 +4710,7 @@ function GamepadMapper({
|
|
|
4743
4710
|
[listeningFor]: i
|
|
4744
4711
|
}
|
|
4745
4712
|
}));
|
|
4746
|
-
|
|
4713
|
+
stopListening();
|
|
4747
4714
|
return;
|
|
4748
4715
|
}
|
|
4749
4716
|
}
|
|
@@ -4756,21 +4723,7 @@ function GamepadMapper({
|
|
|
4756
4723
|
cancelAnimationFrame(rafRef.current);
|
|
4757
4724
|
}
|
|
4758
4725
|
};
|
|
4759
|
-
}, [isOpen, listeningFor, selectedPlayer]);
|
|
4760
|
-
React2.useEffect(() => {
|
|
4761
|
-
if (!isOpen) return;
|
|
4762
|
-
const handleKeyDown = (e) => {
|
|
4763
|
-
if (e.code === "Escape") {
|
|
4764
|
-
if (listeningFor) {
|
|
4765
|
-
setListeningFor(null);
|
|
4766
|
-
} else {
|
|
4767
|
-
onClose();
|
|
4768
|
-
}
|
|
4769
|
-
}
|
|
4770
|
-
};
|
|
4771
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
4772
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4773
|
-
}, [isOpen, listeningFor, onClose]);
|
|
4726
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4774
4727
|
const handleReset = () => {
|
|
4775
4728
|
setBindings((prev) => ({
|
|
4776
4729
|
...prev,
|
|
@@ -4785,127 +4738,19 @@ function GamepadMapper({
|
|
|
4785
4738
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4786
4739
|
onClose();
|
|
4787
4740
|
};
|
|
4788
|
-
if (!isOpen) return null;
|
|
4789
4741
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4790
4742
|
const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
}
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4802
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4803
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
|
|
4804
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4805
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
|
|
4806
|
-
/* @__PURE__ */ jsxRuntime.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 })
|
|
4807
|
-
] })
|
|
4808
|
-
] }),
|
|
4809
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4810
|
-
"button",
|
|
4811
|
-
{
|
|
4812
|
-
onClick: onClose,
|
|
4813
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4814
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
4815
|
-
}
|
|
4816
|
-
)
|
|
4817
|
-
] }),
|
|
4818
|
-
gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4819
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4820
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4821
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4822
|
-
"button",
|
|
4823
|
-
{
|
|
4824
|
-
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4825
|
-
className: `
|
|
4826
|
-
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4827
|
-
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4828
|
-
`,
|
|
4829
|
-
style: selectedPlayer === gp.index + 1 ? {
|
|
4830
|
-
backgroundColor: `${systemColor}20`,
|
|
4831
|
-
color: systemColor
|
|
4832
|
-
} : {},
|
|
4833
|
-
children: [
|
|
4834
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
|
|
4835
|
-
"P",
|
|
4836
|
-
gp.index + 1
|
|
4837
|
-
]
|
|
4838
|
-
},
|
|
4839
|
-
gp.index
|
|
4840
|
-
)) })
|
|
4841
|
-
] }),
|
|
4842
|
-
currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4843
|
-
] }),
|
|
4844
|
-
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4845
|
-
currentGamepad.name,
|
|
4846
|
-
" \u2022 Player 1"
|
|
4847
|
-
] }) }),
|
|
4848
|
-
gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4849
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4850
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4851
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4852
|
-
] }),
|
|
4853
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4854
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4855
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4856
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4857
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
4858
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4859
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4860
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4861
|
-
] })
|
|
4862
|
-
] })
|
|
4863
|
-
] }),
|
|
4864
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4865
|
-
listeningFor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4866
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4867
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4868
|
-
] }),
|
|
4869
|
-
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4870
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4871
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4872
|
-
"button",
|
|
4873
|
-
{
|
|
4874
|
-
onClick: () => setListeningFor(btn),
|
|
4875
|
-
disabled: !!listeningFor && listeningFor !== btn,
|
|
4876
|
-
className: `
|
|
4877
|
-
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4878
|
-
disabled:opacity-50
|
|
4879
|
-
${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"}
|
|
4880
|
-
`,
|
|
4881
|
-
style: listeningFor === btn ? {
|
|
4882
|
-
borderColor: systemColor,
|
|
4883
|
-
backgroundColor: `${systemColor}20`,
|
|
4884
|
-
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4885
|
-
} : {},
|
|
4886
|
-
children: [
|
|
4887
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4888
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4889
|
-
"span",
|
|
4890
|
-
{
|
|
4891
|
-
className: `
|
|
4892
|
-
px-2 py-1 rounded text-xs font-mono
|
|
4893
|
-
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4894
|
-
`,
|
|
4895
|
-
style: listeningFor === btn ? {
|
|
4896
|
-
backgroundColor: `${systemColor}30`,
|
|
4897
|
-
color: systemColor
|
|
4898
|
-
} : {},
|
|
4899
|
-
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4900
|
-
}
|
|
4901
|
-
)
|
|
4902
|
-
]
|
|
4903
|
-
},
|
|
4904
|
-
btn
|
|
4905
|
-
)) })
|
|
4906
|
-
] }, group.label))
|
|
4907
|
-
] }),
|
|
4908
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4743
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4744
|
+
ModalShell,
|
|
4745
|
+
{
|
|
4746
|
+
isOpen,
|
|
4747
|
+
onClose,
|
|
4748
|
+
title: t.modals.gamepad.title,
|
|
4749
|
+
subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
|
|
4750
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 24, style: { color: systemColor } }),
|
|
4751
|
+
systemColor,
|
|
4752
|
+
closeOnBackdrop: !isListening,
|
|
4753
|
+
footer: gamepads.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4909
4754
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4910
4755
|
"button",
|
|
4911
4756
|
{
|
|
@@ -4931,9 +4776,101 @@ function GamepadMapper({
|
|
|
4931
4776
|
]
|
|
4932
4777
|
}
|
|
4933
4778
|
)
|
|
4934
|
-
] })
|
|
4935
|
-
|
|
4936
|
-
|
|
4779
|
+
] }) : void 0,
|
|
4780
|
+
children: [
|
|
4781
|
+
gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4782
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4783
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4784
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4785
|
+
"button",
|
|
4786
|
+
{
|
|
4787
|
+
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4788
|
+
className: `
|
|
4789
|
+
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4790
|
+
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4791
|
+
`,
|
|
4792
|
+
style: selectedPlayer === gp.index + 1 ? {
|
|
4793
|
+
backgroundColor: `${systemColor}20`,
|
|
4794
|
+
color: systemColor
|
|
4795
|
+
} : {},
|
|
4796
|
+
children: [
|
|
4797
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
|
|
4798
|
+
"P",
|
|
4799
|
+
gp.index + 1
|
|
4800
|
+
]
|
|
4801
|
+
},
|
|
4802
|
+
gp.index
|
|
4803
|
+
)) })
|
|
4804
|
+
] }),
|
|
4805
|
+
currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4806
|
+
] }),
|
|
4807
|
+
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4808
|
+
currentGamepad.name,
|
|
4809
|
+
" \u2022 Player 1"
|
|
4810
|
+
] }) }),
|
|
4811
|
+
gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4812
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4813
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4814
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4815
|
+
] }),
|
|
4816
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4817
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4818
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4819
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4820
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
4821
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4822
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4823
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4824
|
+
] })
|
|
4825
|
+
] })
|
|
4826
|
+
] }),
|
|
4827
|
+
gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4828
|
+
listeningFor && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4829
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4830
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4831
|
+
] }),
|
|
4832
|
+
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4833
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4834
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4835
|
+
"button",
|
|
4836
|
+
{
|
|
4837
|
+
onClick: () => startListening(btn),
|
|
4838
|
+
disabled: !!listeningFor && listeningFor !== btn,
|
|
4839
|
+
className: `
|
|
4840
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4841
|
+
disabled:opacity-50
|
|
4842
|
+
${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"}
|
|
4843
|
+
`,
|
|
4844
|
+
style: listeningFor === btn ? {
|
|
4845
|
+
borderColor: systemColor,
|
|
4846
|
+
backgroundColor: `${systemColor}20`,
|
|
4847
|
+
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4848
|
+
} : {},
|
|
4849
|
+
children: [
|
|
4850
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4851
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4852
|
+
"span",
|
|
4853
|
+
{
|
|
4854
|
+
className: `
|
|
4855
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4856
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4857
|
+
`,
|
|
4858
|
+
style: listeningFor === btn ? {
|
|
4859
|
+
backgroundColor: `${systemColor}30`,
|
|
4860
|
+
color: systemColor
|
|
4861
|
+
} : {},
|
|
4862
|
+
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4863
|
+
}
|
|
4864
|
+
)
|
|
4865
|
+
]
|
|
4866
|
+
},
|
|
4867
|
+
btn
|
|
4868
|
+
)) })
|
|
4869
|
+
] }, group.label))
|
|
4870
|
+
] })
|
|
4871
|
+
]
|
|
4872
|
+
}
|
|
4873
|
+
);
|
|
4937
4874
|
}
|
|
4938
4875
|
function CheatModal({
|
|
4939
4876
|
isOpen,
|
|
@@ -4941,42 +4878,24 @@ function CheatModal({
|
|
|
4941
4878
|
activeCheats,
|
|
4942
4879
|
onToggle,
|
|
4943
4880
|
onClose
|
|
4944
|
-
}) {
|
|
4945
|
-
const t = useKoinTranslation();
|
|
4946
|
-
const [copiedId, setCopiedId] = React2__default.default.useState(null);
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
setCopiedId(
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
}
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
|
|
4963
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4964
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { className: "text-purple-400", size: 24 }),
|
|
4965
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4966
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
|
|
4967
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
|
|
4968
|
-
] })
|
|
4969
|
-
] }),
|
|
4970
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4971
|
-
"button",
|
|
4972
|
-
{
|
|
4973
|
-
onClick: onClose,
|
|
4974
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4975
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
4976
|
-
}
|
|
4977
|
-
)
|
|
4978
|
-
] }),
|
|
4979
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4881
|
+
}) {
|
|
4882
|
+
const t = useKoinTranslation();
|
|
4883
|
+
const [copiedId, setCopiedId] = React2__default.default.useState(null);
|
|
4884
|
+
const handleCopy = async (code, id) => {
|
|
4885
|
+
await navigator.clipboard.writeText(code);
|
|
4886
|
+
setCopiedId(id);
|
|
4887
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
4888
|
+
};
|
|
4889
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4890
|
+
ModalShell,
|
|
4891
|
+
{
|
|
4892
|
+
isOpen,
|
|
4893
|
+
onClose,
|
|
4894
|
+
title: t.modals.cheats.title,
|
|
4895
|
+
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4896
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
|
|
4897
|
+
footer: /* @__PURE__ */ jsxRuntime.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 }),
|
|
4898
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4980
4899
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4981
4900
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4982
4901
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4986,18 +4905,18 @@ function CheatModal({
|
|
|
4986
4905
|
"div",
|
|
4987
4906
|
{
|
|
4988
4907
|
className: `
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4908
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
4909
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4910
|
+
`,
|
|
4992
4911
|
onClick: () => onToggle(cheat.id),
|
|
4993
4912
|
children: [
|
|
4994
4913
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4995
4914
|
"div",
|
|
4996
4915
|
{
|
|
4997
4916
|
className: `
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
4917
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4918
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4919
|
+
`,
|
|
5001
4920
|
children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
|
|
5002
4921
|
}
|
|
5003
4922
|
),
|
|
@@ -5023,10 +4942,9 @@ function CheatModal({
|
|
|
5023
4942
|
},
|
|
5024
4943
|
cheat.id
|
|
5025
4944
|
);
|
|
5026
|
-
}) })
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
] });
|
|
4945
|
+
}) })
|
|
4946
|
+
}
|
|
4947
|
+
);
|
|
5030
4948
|
}
|
|
5031
4949
|
var AUTO_SAVE_SLOT = 5;
|
|
5032
4950
|
function formatBytes(bytes) {
|
|
@@ -5067,7 +4985,6 @@ function SaveSlotModal({
|
|
|
5067
4985
|
onUpgrade
|
|
5068
4986
|
}) {
|
|
5069
4987
|
const t = useKoinTranslation();
|
|
5070
|
-
if (!isOpen) return null;
|
|
5071
4988
|
const isSaveMode = mode === "save";
|
|
5072
4989
|
const allSlots = [1, 2, 3, 4, 5];
|
|
5073
4990
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -5082,33 +4999,17 @@ function SaveSlotModal({
|
|
|
5082
4999
|
const getSlotData = (slotNum) => {
|
|
5083
5000
|
return slots.find((s) => s.slot === slotNum);
|
|
5084
5001
|
};
|
|
5085
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5095
|
-
|
|
5096
|
-
isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
|
|
5097
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5098
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
|
|
5099
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
|
|
5100
|
-
] })
|
|
5101
|
-
] }),
|
|
5102
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5103
|
-
"button",
|
|
5104
|
-
{
|
|
5105
|
-
onClick: onClose,
|
|
5106
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5107
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
5108
|
-
}
|
|
5109
|
-
)
|
|
5110
|
-
] }),
|
|
5111
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
5002
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5003
|
+
ModalShell,
|
|
5004
|
+
{
|
|
5005
|
+
isOpen,
|
|
5006
|
+
onClose,
|
|
5007
|
+
title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
|
|
5008
|
+
subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
|
|
5009
|
+
icon: isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
|
|
5010
|
+
maxWidth: "md",
|
|
5011
|
+
footer: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
|
|
5012
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
5112
5013
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
5113
5014
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
5114
5015
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5227,10 +5128,9 @@ function SaveSlotModal({
|
|
|
5227
5128
|
},
|
|
5228
5129
|
slotNum
|
|
5229
5130
|
);
|
|
5230
|
-
}) })
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
] });
|
|
5131
|
+
}) })
|
|
5132
|
+
}
|
|
5133
|
+
);
|
|
5234
5134
|
}
|
|
5235
5135
|
function BiosSelectionModal({
|
|
5236
5136
|
isOpen,
|
|
@@ -5377,36 +5277,28 @@ function SettingsModal({
|
|
|
5377
5277
|
systemColor = "#00FF41"
|
|
5378
5278
|
}) {
|
|
5379
5279
|
const t = useKoinTranslation();
|
|
5380
|
-
if (!isOpen) return null;
|
|
5381
5280
|
const languages = [
|
|
5382
5281
|
{ code: "en", name: "English" },
|
|
5383
5282
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5384
5283
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5385
5284
|
];
|
|
5386
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
}
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
onClick: onClose,
|
|
5404
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5405
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
5406
|
-
}
|
|
5407
|
-
)
|
|
5408
|
-
] }),
|
|
5409
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5285
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5286
|
+
ModalShell,
|
|
5287
|
+
{
|
|
5288
|
+
isOpen,
|
|
5289
|
+
onClose,
|
|
5290
|
+
title: t.settings.title,
|
|
5291
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { size: 20, className: "text-white" }),
|
|
5292
|
+
maxWidth: "sm",
|
|
5293
|
+
footer: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5294
|
+
"button",
|
|
5295
|
+
{
|
|
5296
|
+
onClick: onClose,
|
|
5297
|
+
className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
|
|
5298
|
+
children: t.modals.shortcuts.pressEsc
|
|
5299
|
+
}
|
|
5300
|
+
),
|
|
5301
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5410
5302
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5411
5303
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
|
|
5412
5304
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
|
|
@@ -5418,9 +5310,9 @@ function SettingsModal({
|
|
|
5418
5310
|
{
|
|
5419
5311
|
onClick: () => onLanguageChange(lang.code),
|
|
5420
5312
|
className: `
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5313
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5314
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5315
|
+
`,
|
|
5424
5316
|
children: [
|
|
5425
5317
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
|
|
5426
5318
|
isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5429,17 +5321,9 @@ function SettingsModal({
|
|
|
5429
5321
|
lang.code
|
|
5430
5322
|
);
|
|
5431
5323
|
}) })
|
|
5432
|
-
] }) })
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
{
|
|
5436
|
-
onClick: onClose,
|
|
5437
|
-
className: "text-sm text-gray-500 hover:text-white transition-colors",
|
|
5438
|
-
children: t.modals.shortcuts.pressEsc
|
|
5439
|
-
}
|
|
5440
|
-
) })
|
|
5441
|
-
] })
|
|
5442
|
-
] });
|
|
5324
|
+
] }) })
|
|
5325
|
+
}
|
|
5326
|
+
);
|
|
5443
5327
|
}
|
|
5444
5328
|
function GameModals({
|
|
5445
5329
|
controlsModalOpen,
|
|
@@ -7650,6 +7534,171 @@ var useNostalgist = ({
|
|
|
7650
7534
|
]);
|
|
7651
7535
|
return hookReturn;
|
|
7652
7536
|
};
|
|
7537
|
+
function getDisplayName(id) {
|
|
7538
|
+
let name = id;
|
|
7539
|
+
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
7540
|
+
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
7541
|
+
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
7542
|
+
if (/xbox/i.test(name)) {
|
|
7543
|
+
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
7544
|
+
if (/one/i.test(name)) return "Xbox One Controller";
|
|
7545
|
+
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
7546
|
+
return "Xbox Controller";
|
|
7547
|
+
}
|
|
7548
|
+
if (/dualsense/i.test(name)) return "DualSense";
|
|
7549
|
+
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
7550
|
+
if (/dualshock/i.test(name)) return "DualShock";
|
|
7551
|
+
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
7552
|
+
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
7553
|
+
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
7554
|
+
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
7555
|
+
return name.trim() || "Gamepad";
|
|
7556
|
+
}
|
|
7557
|
+
function detectControllerBrand(id) {
|
|
7558
|
+
const lowerId = id.toLowerCase();
|
|
7559
|
+
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
7560
|
+
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
7561
|
+
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
7562
|
+
return "generic";
|
|
7563
|
+
}
|
|
7564
|
+
function toGamepadInfo(gamepad) {
|
|
7565
|
+
return {
|
|
7566
|
+
index: gamepad.index,
|
|
7567
|
+
id: gamepad.id,
|
|
7568
|
+
name: getDisplayName(gamepad.id),
|
|
7569
|
+
connected: gamepad.connected,
|
|
7570
|
+
buttons: gamepad.buttons.length,
|
|
7571
|
+
axes: gamepad.axes.length,
|
|
7572
|
+
mapping: gamepad.mapping
|
|
7573
|
+
};
|
|
7574
|
+
}
|
|
7575
|
+
function useGamepad(options) {
|
|
7576
|
+
const { onConnect, onDisconnect } = options || {};
|
|
7577
|
+
const [gamepads, setGamepads] = React2.useState([]);
|
|
7578
|
+
const rafRef = React2.useRef(null);
|
|
7579
|
+
const lastStateRef = React2.useRef("");
|
|
7580
|
+
const prevCountRef = React2.useRef(0);
|
|
7581
|
+
const onConnectRef = React2.useRef(onConnect);
|
|
7582
|
+
const onDisconnectRef = React2.useRef(onDisconnect);
|
|
7583
|
+
React2.useEffect(() => {
|
|
7584
|
+
onConnectRef.current = onConnect;
|
|
7585
|
+
onDisconnectRef.current = onDisconnect;
|
|
7586
|
+
}, [onConnect, onDisconnect]);
|
|
7587
|
+
const getGamepads = React2.useCallback(() => {
|
|
7588
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
7589
|
+
return [];
|
|
7590
|
+
}
|
|
7591
|
+
const rawGamepads = navigator.getGamepads() ?? [];
|
|
7592
|
+
const connected = [];
|
|
7593
|
+
for (let i = 0; i < rawGamepads.length; i++) {
|
|
7594
|
+
const gp = rawGamepads[i];
|
|
7595
|
+
if (gp && gp.connected) {
|
|
7596
|
+
connected.push(toGamepadInfo(gp));
|
|
7597
|
+
}
|
|
7598
|
+
}
|
|
7599
|
+
return connected;
|
|
7600
|
+
}, []);
|
|
7601
|
+
const getRawGamepad = React2.useCallback((index) => {
|
|
7602
|
+
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
7603
|
+
return rawGamepads[index] ?? null;
|
|
7604
|
+
}, []);
|
|
7605
|
+
const refresh = React2.useCallback(() => {
|
|
7606
|
+
setGamepads(getGamepads());
|
|
7607
|
+
}, [getGamepads]);
|
|
7608
|
+
React2.useEffect(() => {
|
|
7609
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
if (typeof navigator.getGamepads !== "function") {
|
|
7613
|
+
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
7614
|
+
return;
|
|
7615
|
+
}
|
|
7616
|
+
let isActive = true;
|
|
7617
|
+
const poll = () => {
|
|
7618
|
+
if (!isActive) return;
|
|
7619
|
+
const current = getGamepads();
|
|
7620
|
+
let hasChanged = current.length !== prevCountRef.current;
|
|
7621
|
+
if (!hasChanged) {
|
|
7622
|
+
for (let i = 0; i < current.length; i++) {
|
|
7623
|
+
const saved = gamepads[i];
|
|
7624
|
+
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
7625
|
+
hasChanged = true;
|
|
7626
|
+
break;
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
if (hasChanged) {
|
|
7631
|
+
const prevCount = prevCountRef.current;
|
|
7632
|
+
const currentCount = current.length;
|
|
7633
|
+
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
7634
|
+
const newGamepad = current[current.length - 1];
|
|
7635
|
+
onConnectRef.current(newGamepad);
|
|
7636
|
+
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
7637
|
+
onDisconnectRef.current();
|
|
7638
|
+
}
|
|
7639
|
+
prevCountRef.current = currentCount;
|
|
7640
|
+
setGamepads(current);
|
|
7641
|
+
}
|
|
7642
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7643
|
+
};
|
|
7644
|
+
const handleConnect = (e) => {
|
|
7645
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
7646
|
+
const current = getGamepads();
|
|
7647
|
+
const prevCount = prevCountRef.current;
|
|
7648
|
+
prevCountRef.current = current.length;
|
|
7649
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7650
|
+
setGamepads(current);
|
|
7651
|
+
if (onConnectRef.current && current.length > prevCount) {
|
|
7652
|
+
const newGamepad = current[current.length - 1];
|
|
7653
|
+
onConnectRef.current(newGamepad);
|
|
7654
|
+
}
|
|
7655
|
+
};
|
|
7656
|
+
const handleDisconnect = (e) => {
|
|
7657
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
7658
|
+
const current = getGamepads();
|
|
7659
|
+
const prevCount = prevCountRef.current;
|
|
7660
|
+
prevCountRef.current = current.length;
|
|
7661
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7662
|
+
setGamepads(current);
|
|
7663
|
+
if (onDisconnectRef.current && current.length < prevCount) {
|
|
7664
|
+
onDisconnectRef.current();
|
|
7665
|
+
}
|
|
7666
|
+
};
|
|
7667
|
+
window.addEventListener("gamepadconnected", handleConnect);
|
|
7668
|
+
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
7669
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7670
|
+
const initial = getGamepads();
|
|
7671
|
+
if (initial.length > 0) {
|
|
7672
|
+
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
7673
|
+
prevCountRef.current = initial.length;
|
|
7674
|
+
setGamepads(initial);
|
|
7675
|
+
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7676
|
+
} else {
|
|
7677
|
+
prevCountRef.current = 0;
|
|
7678
|
+
}
|
|
7679
|
+
return () => {
|
|
7680
|
+
isActive = false;
|
|
7681
|
+
if (rafRef.current) {
|
|
7682
|
+
cancelAnimationFrame(rafRef.current);
|
|
7683
|
+
}
|
|
7684
|
+
window.removeEventListener("gamepadconnected", handleConnect);
|
|
7685
|
+
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
7686
|
+
};
|
|
7687
|
+
}, [getGamepads]);
|
|
7688
|
+
return {
|
|
7689
|
+
gamepads,
|
|
7690
|
+
isAnyConnected: gamepads.length > 0,
|
|
7691
|
+
connectedCount: gamepads.length,
|
|
7692
|
+
getRawGamepad,
|
|
7693
|
+
refresh
|
|
7694
|
+
};
|
|
7695
|
+
}
|
|
7696
|
+
var STANDARD_AXIS_MAP = {
|
|
7697
|
+
leftStickX: 0,
|
|
7698
|
+
leftStickY: 1,
|
|
7699
|
+
rightStickX: 2,
|
|
7700
|
+
rightStickY: 3
|
|
7701
|
+
};
|
|
7653
7702
|
function useVolume({
|
|
7654
7703
|
setVolume: setVolumeInHook,
|
|
7655
7704
|
toggleMute: toggleMuteInHook
|
|
@@ -9537,27 +9586,15 @@ function AchievementPopup({
|
|
|
9537
9586
|
onDismiss,
|
|
9538
9587
|
autoDismissMs = 5e3
|
|
9539
9588
|
}) {
|
|
9540
|
-
const
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
});
|
|
9546
|
-
const timer = setTimeout(() => {
|
|
9547
|
-
handleDismiss();
|
|
9548
|
-
}, autoDismissMs);
|
|
9549
|
-
return () => clearTimeout(timer);
|
|
9550
|
-
}, [autoDismissMs]);
|
|
9551
|
-
const handleDismiss = () => {
|
|
9552
|
-
setIsExiting(true);
|
|
9553
|
-
setTimeout(() => {
|
|
9554
|
-
onDismiss();
|
|
9555
|
-
}, 300);
|
|
9556
|
-
};
|
|
9589
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9590
|
+
exitDuration: 300,
|
|
9591
|
+
onExit: onDismiss,
|
|
9592
|
+
autoDismissMs
|
|
9593
|
+
});
|
|
9557
9594
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9558
9595
|
"div",
|
|
9559
9596
|
{
|
|
9560
|
-
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${
|
|
9597
|
+
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
|
|
9561
9598
|
children: [
|
|
9562
9599
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
|
|
9563
9600
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative bg-gradient-to-r from-yellow-500 to-orange-500 p-[2px] rounded-lg", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-gray-900 rounded-lg p-4 flex items-center gap-4 min-w-[320px]", children: [
|
|
@@ -9594,7 +9631,7 @@ function AchievementPopup({
|
|
|
9594
9631
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9595
9632
|
"button",
|
|
9596
9633
|
{
|
|
9597
|
-
onClick:
|
|
9634
|
+
onClick: triggerExit,
|
|
9598
9635
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9599
9636
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18 })
|
|
9600
9637
|
}
|