koin.js 1.0.14 → 1.0.16
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 +698 -645
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +699 -646
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -827,7 +827,7 @@ var SaveLoadControls = React2.memo(function SaveLoadControls2({
|
|
|
827
827
|
{
|
|
828
828
|
progress: autoSaveProgress,
|
|
829
829
|
state: autoSavePaused ? "idle" : autoSaveState,
|
|
830
|
-
intervalSeconds:
|
|
830
|
+
intervalSeconds: 60,
|
|
831
831
|
isPaused: autoSavePaused,
|
|
832
832
|
onClick: onAutoSaveToggle
|
|
833
833
|
}
|
|
@@ -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,
|
|
@@ -3635,14 +3707,22 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3635
3707
|
if (!touch) return;
|
|
3636
3708
|
if (drag.isDragging) {
|
|
3637
3709
|
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3710
|
+
} else if (onPositionChange) {
|
|
3711
|
+
const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
|
|
3712
|
+
if (!startedDrag) {
|
|
3713
|
+
drag.clearDragTimer();
|
|
3714
|
+
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3715
|
+
if (rect) {
|
|
3716
|
+
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3638
3719
|
} else {
|
|
3639
3720
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3640
3721
|
if (rect) {
|
|
3641
|
-
drag.clearDragTimer();
|
|
3642
3722
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3643
3723
|
}
|
|
3644
3724
|
}
|
|
3645
|
-
}, [drag, getDirectionsFromTouch, updateDirections]);
|
|
3725
|
+
}, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
|
|
3646
3726
|
const handleTouchEnd = React2.useCallback((e) => {
|
|
3647
3727
|
e.preventDefault();
|
|
3648
3728
|
drag.clearDragTimer();
|
|
@@ -3667,21 +3747,12 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3667
3747
|
}
|
|
3668
3748
|
}
|
|
3669
3749
|
}, [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]);
|
|
3750
|
+
useTouchEvents(dpadRef, {
|
|
3751
|
+
onTouchStart: handleTouchStart,
|
|
3752
|
+
onTouchMove: handleTouchMove,
|
|
3753
|
+
onTouchEnd: handleTouchEnd,
|
|
3754
|
+
onTouchCancel: handleTouchEnd
|
|
3755
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3685
3756
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3686
3757
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3687
3758
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3852,8 +3923,16 @@ function ControlsHint({ isVisible }) {
|
|
|
3852
3923
|
children: [
|
|
3853
3924
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Move, { size: 32, className: "text-green-400" }) }) }),
|
|
3854
3925
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
|
|
3855
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-
|
|
3856
|
-
|
|
3926
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3927
|
+
"Use the ",
|
|
3928
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Lock, { size: 12, className: "inline mx-1 text-white" }),
|
|
3929
|
+
" ",
|
|
3930
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "lock icon" }),
|
|
3931
|
+
" at the top to unlock controls for repositioning."
|
|
3932
|
+
] }),
|
|
3933
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3934
|
+
"When unlocked, ",
|
|
3935
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "long-press" }),
|
|
3857
3936
|
" any button or the ",
|
|
3858
3937
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-white", children: "D-pad center" }),
|
|
3859
3938
|
" to drag and reposition it."
|
|
@@ -3881,6 +3960,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3881
3960
|
}
|
|
3882
3961
|
);
|
|
3883
3962
|
}
|
|
3963
|
+
function LockButton({
|
|
3964
|
+
isLocked,
|
|
3965
|
+
onToggle,
|
|
3966
|
+
systemColor = "#00FF41"
|
|
3967
|
+
}) {
|
|
3968
|
+
const Icon = isLocked ? lucideReact.Lock : lucideReact.Unlock;
|
|
3969
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3970
|
+
"button",
|
|
3971
|
+
{
|
|
3972
|
+
onClick: onToggle,
|
|
3973
|
+
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",
|
|
3974
|
+
style: {
|
|
3975
|
+
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3976
|
+
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
3977
|
+
},
|
|
3978
|
+
"aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
|
|
3979
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3980
|
+
Icon,
|
|
3981
|
+
{
|
|
3982
|
+
size: 18,
|
|
3983
|
+
style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
|
|
3984
|
+
}
|
|
3985
|
+
)
|
|
3986
|
+
}
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
var LOCK_KEY = "koin-controls-locked";
|
|
3884
3990
|
function VirtualController({
|
|
3885
3991
|
system,
|
|
3886
3992
|
isRunning,
|
|
@@ -3892,7 +3998,21 @@ function VirtualController({
|
|
|
3892
3998
|
const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
3893
3999
|
const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
|
|
3894
4000
|
const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
|
|
4001
|
+
const [isLocked, setIsLocked] = React2.useState(true);
|
|
3895
4002
|
const { getPosition, savePosition } = useButtonPositions();
|
|
4003
|
+
React2.useEffect(() => {
|
|
4004
|
+
const stored = localStorage.getItem(LOCK_KEY);
|
|
4005
|
+
if (stored !== null) {
|
|
4006
|
+
setIsLocked(stored === "true");
|
|
4007
|
+
}
|
|
4008
|
+
}, []);
|
|
4009
|
+
const toggleLock = React2.useCallback(() => {
|
|
4010
|
+
setIsLocked((prev) => {
|
|
4011
|
+
const newValue = !prev;
|
|
4012
|
+
localStorage.setItem(LOCK_KEY, String(newValue));
|
|
4013
|
+
return newValue;
|
|
4014
|
+
});
|
|
4015
|
+
}, []);
|
|
3896
4016
|
const layout = getLayoutForSystem(system);
|
|
3897
4017
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3898
4018
|
if (isPortrait) {
|
|
@@ -4047,6 +4167,14 @@ function VirtualController({
|
|
|
4047
4167
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
4048
4168
|
style: { touchAction: "none" },
|
|
4049
4169
|
children: [
|
|
4170
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4171
|
+
LockButton,
|
|
4172
|
+
{
|
|
4173
|
+
isLocked,
|
|
4174
|
+
onToggle: toggleLock,
|
|
4175
|
+
systemColor
|
|
4176
|
+
}
|
|
4177
|
+
),
|
|
4050
4178
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4051
4179
|
Dpad_default,
|
|
4052
4180
|
{
|
|
@@ -4059,7 +4187,7 @@ function VirtualController({
|
|
|
4059
4187
|
systemColor,
|
|
4060
4188
|
isLandscape,
|
|
4061
4189
|
customPosition: getPosition("up", isLandscape),
|
|
4062
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4190
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
4063
4191
|
}
|
|
4064
4192
|
),
|
|
4065
4193
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4073,7 +4201,7 @@ function VirtualController({
|
|
|
4073
4201
|
containerWidth: width,
|
|
4074
4202
|
containerHeight: height,
|
|
4075
4203
|
customPosition,
|
|
4076
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4204
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4077
4205
|
isLandscape,
|
|
4078
4206
|
console: layout.console
|
|
4079
4207
|
},
|
|
@@ -4393,6 +4521,45 @@ var GameCanvas = React2.memo(function GameCanvas2({
|
|
|
4393
4521
|
] });
|
|
4394
4522
|
});
|
|
4395
4523
|
var GameCanvas_default = GameCanvas;
|
|
4524
|
+
function useInputCapture({
|
|
4525
|
+
isOpen,
|
|
4526
|
+
onClose
|
|
4527
|
+
}) {
|
|
4528
|
+
const [listeningFor, setListeningFor] = React2.useState(null);
|
|
4529
|
+
const startListening = React2.useCallback((target) => {
|
|
4530
|
+
setListeningFor(target);
|
|
4531
|
+
}, []);
|
|
4532
|
+
const stopListening = React2.useCallback(() => {
|
|
4533
|
+
setListeningFor(null);
|
|
4534
|
+
}, []);
|
|
4535
|
+
React2.useEffect(() => {
|
|
4536
|
+
if (!isOpen) {
|
|
4537
|
+
setListeningFor(null);
|
|
4538
|
+
}
|
|
4539
|
+
}, [isOpen]);
|
|
4540
|
+
React2.useEffect(() => {
|
|
4541
|
+
if (!isOpen) return;
|
|
4542
|
+
const handleKeyDown = (e) => {
|
|
4543
|
+
if (e.code === "Escape") {
|
|
4544
|
+
if (listeningFor !== null) {
|
|
4545
|
+
e.preventDefault();
|
|
4546
|
+
e.stopPropagation();
|
|
4547
|
+
setListeningFor(null);
|
|
4548
|
+
} else {
|
|
4549
|
+
onClose();
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
};
|
|
4553
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4554
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4555
|
+
}, [isOpen, listeningFor, onClose]);
|
|
4556
|
+
return {
|
|
4557
|
+
listeningFor,
|
|
4558
|
+
startListening,
|
|
4559
|
+
stopListening,
|
|
4560
|
+
isListening: listeningFor !== null
|
|
4561
|
+
};
|
|
4562
|
+
}
|
|
4396
4563
|
function getFilteredGroups(activeButtons) {
|
|
4397
4564
|
return BUTTON_GROUPS.map((group) => ({
|
|
4398
4565
|
...group,
|
|
@@ -4408,7 +4575,10 @@ function ControlMapper({
|
|
|
4408
4575
|
}) {
|
|
4409
4576
|
const t = useKoinTranslation();
|
|
4410
4577
|
const [localControls, setLocalControls] = React2.useState(controls);
|
|
4411
|
-
const
|
|
4578
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4579
|
+
isOpen,
|
|
4580
|
+
onClose
|
|
4581
|
+
});
|
|
4412
4582
|
const activeButtons = React2.useMemo(() => {
|
|
4413
4583
|
return getConsoleButtons(system || "SNES");
|
|
4414
4584
|
}, [system]);
|
|
@@ -4424,27 +4594,20 @@ function ControlMapper({
|
|
|
4424
4594
|
}
|
|
4425
4595
|
}, [isOpen, controls]);
|
|
4426
4596
|
React2.useEffect(() => {
|
|
4427
|
-
if (!isOpen)
|
|
4428
|
-
setListeningFor(null);
|
|
4429
|
-
return;
|
|
4430
|
-
}
|
|
4597
|
+
if (!isOpen || !listeningFor) return;
|
|
4431
4598
|
const handleKeyDown = (e) => {
|
|
4432
|
-
if (
|
|
4599
|
+
if (e.code === "Escape") return;
|
|
4433
4600
|
e.preventDefault();
|
|
4434
4601
|
e.stopPropagation();
|
|
4435
|
-
if (e.code === "Escape") {
|
|
4436
|
-
setListeningFor(null);
|
|
4437
|
-
return;
|
|
4438
|
-
}
|
|
4439
4602
|
setLocalControls((prev) => ({
|
|
4440
4603
|
...prev,
|
|
4441
4604
|
[listeningFor]: e.code
|
|
4442
4605
|
}));
|
|
4443
|
-
|
|
4606
|
+
stopListening();
|
|
4444
4607
|
};
|
|
4445
4608
|
window.addEventListener("keydown", handleKeyDown);
|
|
4446
4609
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4447
|
-
}, [isOpen, listeningFor]);
|
|
4610
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4448
4611
|
const handleReset = () => {
|
|
4449
4612
|
setLocalControls(defaultControls);
|
|
4450
4613
|
};
|
|
@@ -4452,52 +4615,58 @@ function ControlMapper({
|
|
|
4452
4615
|
onSave(localControls);
|
|
4453
4616
|
onClose();
|
|
4454
4617
|
};
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
}
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4618
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4619
|
+
ModalShell,
|
|
4620
|
+
{
|
|
4621
|
+
isOpen,
|
|
4622
|
+
onClose,
|
|
4623
|
+
title: t.modals.controls.title,
|
|
4624
|
+
subtitle: t.modals.controls.description,
|
|
4625
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Gamepad2, { className: "text-retro-primary", size: 24 }),
|
|
4626
|
+
closeOnBackdrop: !isListening,
|
|
4627
|
+
footer: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4628
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4629
|
+
"button",
|
|
4630
|
+
{
|
|
4631
|
+
onClick: handleReset,
|
|
4632
|
+
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",
|
|
4633
|
+
children: [
|
|
4634
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
|
|
4635
|
+
t.modals.controls.reset
|
|
4636
|
+
]
|
|
4637
|
+
}
|
|
4638
|
+
),
|
|
4639
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4474
4640
|
"button",
|
|
4475
4641
|
{
|
|
4476
|
-
onClick:
|
|
4477
|
-
className: "
|
|
4478
|
-
children:
|
|
4642
|
+
onClick: handleSave,
|
|
4643
|
+
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",
|
|
4644
|
+
children: [
|
|
4645
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
|
|
4646
|
+
t.modals.controls.save
|
|
4647
|
+
]
|
|
4479
4648
|
}
|
|
4480
4649
|
)
|
|
4481
4650
|
] }),
|
|
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: [
|
|
4651
|
+
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
4652
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4484
4653
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4485
4654
|
"button",
|
|
4486
4655
|
{
|
|
4487
|
-
onClick: () =>
|
|
4656
|
+
onClick: () => startListening(btn),
|
|
4488
4657
|
className: `
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4658
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4659
|
+
${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"}
|
|
4660
|
+
`,
|
|
4492
4661
|
children: [
|
|
4493
4662
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4494
4663
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4495
4664
|
"span",
|
|
4496
4665
|
{
|
|
4497
4666
|
className: `
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4667
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4668
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4669
|
+
`,
|
|
4501
4670
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4502
4671
|
}
|
|
4503
4672
|
)
|
|
@@ -4505,199 +4674,10 @@ function ControlMapper({
|
|
|
4505
4674
|
},
|
|
4506
4675
|
btn
|
|
4507
4676
|
)) })
|
|
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 [];
|
|
4589
|
-
}
|
|
4590
|
-
const rawGamepads = navigator.getGamepads() ?? [];
|
|
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
|
+
] }, group.label)) })
|
|
4677
4678
|
}
|
|
4678
|
-
|
|
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
|
-
};
|
|
4679
|
+
);
|
|
4694
4680
|
}
|
|
4695
|
-
var STANDARD_AXIS_MAP = {
|
|
4696
|
-
leftStickX: 0,
|
|
4697
|
-
leftStickY: 1,
|
|
4698
|
-
rightStickX: 2,
|
|
4699
|
-
rightStickY: 3
|
|
4700
|
-
};
|
|
4701
4681
|
function GamepadMapper({
|
|
4702
4682
|
isOpen,
|
|
4703
4683
|
gamepads,
|
|
@@ -4708,7 +4688,10 @@ function GamepadMapper({
|
|
|
4708
4688
|
const t = useKoinTranslation();
|
|
4709
4689
|
const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
|
|
4710
4690
|
const [bindings, setBindings] = React2.useState({});
|
|
4711
|
-
const
|
|
4691
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4692
|
+
isOpen,
|
|
4693
|
+
onClose
|
|
4694
|
+
});
|
|
4712
4695
|
const rafRef = React2.useRef(null);
|
|
4713
4696
|
React2.useEffect(() => {
|
|
4714
4697
|
if (isOpen) {
|
|
@@ -4743,7 +4726,7 @@ function GamepadMapper({
|
|
|
4743
4726
|
[listeningFor]: i
|
|
4744
4727
|
}
|
|
4745
4728
|
}));
|
|
4746
|
-
|
|
4729
|
+
stopListening();
|
|
4747
4730
|
return;
|
|
4748
4731
|
}
|
|
4749
4732
|
}
|
|
@@ -4756,21 +4739,7 @@ function GamepadMapper({
|
|
|
4756
4739
|
cancelAnimationFrame(rafRef.current);
|
|
4757
4740
|
}
|
|
4758
4741
|
};
|
|
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]);
|
|
4742
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4774
4743
|
const handleReset = () => {
|
|
4775
4744
|
setBindings((prev) => ({
|
|
4776
4745
|
...prev,
|
|
@@ -4785,127 +4754,19 @@ function GamepadMapper({
|
|
|
4785
4754
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4786
4755
|
onClose();
|
|
4787
4756
|
};
|
|
4788
|
-
if (!isOpen) return null;
|
|
4789
4757
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4790
4758
|
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: [
|
|
4759
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4760
|
+
ModalShell,
|
|
4761
|
+
{
|
|
4762
|
+
isOpen,
|
|
4763
|
+
onClose,
|
|
4764
|
+
title: t.modals.gamepad.title,
|
|
4765
|
+
subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
|
|
4766
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 24, style: { color: systemColor } }),
|
|
4767
|
+
systemColor,
|
|
4768
|
+
closeOnBackdrop: !isListening,
|
|
4769
|
+
footer: gamepads.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4909
4770
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4910
4771
|
"button",
|
|
4911
4772
|
{
|
|
@@ -4931,9 +4792,101 @@ function GamepadMapper({
|
|
|
4931
4792
|
]
|
|
4932
4793
|
}
|
|
4933
4794
|
)
|
|
4934
|
-
] })
|
|
4935
|
-
|
|
4936
|
-
|
|
4795
|
+
] }) : void 0,
|
|
4796
|
+
children: [
|
|
4797
|
+
gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4798
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4799
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4800
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4801
|
+
"button",
|
|
4802
|
+
{
|
|
4803
|
+
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4804
|
+
className: `
|
|
4805
|
+
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4806
|
+
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4807
|
+
`,
|
|
4808
|
+
style: selectedPlayer === gp.index + 1 ? {
|
|
4809
|
+
backgroundColor: `${systemColor}20`,
|
|
4810
|
+
color: systemColor
|
|
4811
|
+
} : {},
|
|
4812
|
+
children: [
|
|
4813
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
|
|
4814
|
+
"P",
|
|
4815
|
+
gp.index + 1
|
|
4816
|
+
]
|
|
4817
|
+
},
|
|
4818
|
+
gp.index
|
|
4819
|
+
)) })
|
|
4820
|
+
] }),
|
|
4821
|
+
currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4822
|
+
] }),
|
|
4823
|
+
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: [
|
|
4824
|
+
currentGamepad.name,
|
|
4825
|
+
" \u2022 Player 1"
|
|
4826
|
+
] }) }),
|
|
4827
|
+
gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4828
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4829
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4830
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4831
|
+
] }),
|
|
4832
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4833
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4834
|
+
/* @__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: [
|
|
4835
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4836
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
4837
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4838
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4839
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4840
|
+
] })
|
|
4841
|
+
] })
|
|
4842
|
+
] }),
|
|
4843
|
+
gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4844
|
+
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: [
|
|
4845
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4846
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4847
|
+
] }),
|
|
4848
|
+
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4849
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4850
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4851
|
+
"button",
|
|
4852
|
+
{
|
|
4853
|
+
onClick: () => startListening(btn),
|
|
4854
|
+
disabled: !!listeningFor && listeningFor !== btn,
|
|
4855
|
+
className: `
|
|
4856
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4857
|
+
disabled:opacity-50
|
|
4858
|
+
${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"}
|
|
4859
|
+
`,
|
|
4860
|
+
style: listeningFor === btn ? {
|
|
4861
|
+
borderColor: systemColor,
|
|
4862
|
+
backgroundColor: `${systemColor}20`,
|
|
4863
|
+
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4864
|
+
} : {},
|
|
4865
|
+
children: [
|
|
4866
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4867
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4868
|
+
"span",
|
|
4869
|
+
{
|
|
4870
|
+
className: `
|
|
4871
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4872
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4873
|
+
`,
|
|
4874
|
+
style: listeningFor === btn ? {
|
|
4875
|
+
backgroundColor: `${systemColor}30`,
|
|
4876
|
+
color: systemColor
|
|
4877
|
+
} : {},
|
|
4878
|
+
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4879
|
+
}
|
|
4880
|
+
)
|
|
4881
|
+
]
|
|
4882
|
+
},
|
|
4883
|
+
btn
|
|
4884
|
+
)) })
|
|
4885
|
+
] }, group.label))
|
|
4886
|
+
] })
|
|
4887
|
+
]
|
|
4888
|
+
}
|
|
4889
|
+
);
|
|
4937
4890
|
}
|
|
4938
4891
|
function CheatModal({
|
|
4939
4892
|
isOpen,
|
|
@@ -4941,42 +4894,24 @@ function CheatModal({
|
|
|
4941
4894
|
activeCheats,
|
|
4942
4895
|
onToggle,
|
|
4943
4896
|
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: [
|
|
4897
|
+
}) {
|
|
4898
|
+
const t = useKoinTranslation();
|
|
4899
|
+
const [copiedId, setCopiedId] = React2__default.default.useState(null);
|
|
4900
|
+
const handleCopy = async (code, id) => {
|
|
4901
|
+
await navigator.clipboard.writeText(code);
|
|
4902
|
+
setCopiedId(id);
|
|
4903
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
4904
|
+
};
|
|
4905
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4906
|
+
ModalShell,
|
|
4907
|
+
{
|
|
4908
|
+
isOpen,
|
|
4909
|
+
onClose,
|
|
4910
|
+
title: t.modals.cheats.title,
|
|
4911
|
+
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4912
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 24, className: "text-purple-400" }),
|
|
4913
|
+
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 }),
|
|
4914
|
+
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
4915
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4981
4916
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4982
4917
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4986,18 +4921,18 @@ function CheatModal({
|
|
|
4986
4921
|
"div",
|
|
4987
4922
|
{
|
|
4988
4923
|
className: `
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4924
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
4925
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4926
|
+
`,
|
|
4992
4927
|
onClick: () => onToggle(cheat.id),
|
|
4993
4928
|
children: [
|
|
4994
4929
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4995
4930
|
"div",
|
|
4996
4931
|
{
|
|
4997
4932
|
className: `
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
4933
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4934
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4935
|
+
`,
|
|
5001
4936
|
children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
|
|
5002
4937
|
}
|
|
5003
4938
|
),
|
|
@@ -5023,10 +4958,9 @@ function CheatModal({
|
|
|
5023
4958
|
},
|
|
5024
4959
|
cheat.id
|
|
5025
4960
|
);
|
|
5026
|
-
}) })
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
] });
|
|
4961
|
+
}) })
|
|
4962
|
+
}
|
|
4963
|
+
);
|
|
5030
4964
|
}
|
|
5031
4965
|
var AUTO_SAVE_SLOT = 5;
|
|
5032
4966
|
function formatBytes(bytes) {
|
|
@@ -5067,7 +5001,6 @@ function SaveSlotModal({
|
|
|
5067
5001
|
onUpgrade
|
|
5068
5002
|
}) {
|
|
5069
5003
|
const t = useKoinTranslation();
|
|
5070
|
-
if (!isOpen) return null;
|
|
5071
5004
|
const isSaveMode = mode === "save";
|
|
5072
5005
|
const allSlots = [1, 2, 3, 4, 5];
|
|
5073
5006
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -5082,33 +5015,17 @@ function SaveSlotModal({
|
|
|
5082
5015
|
const getSlotData = (slotNum) => {
|
|
5083
5016
|
return slots.find((s) => s.slot === slotNum);
|
|
5084
5017
|
};
|
|
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: [
|
|
5018
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5019
|
+
ModalShell,
|
|
5020
|
+
{
|
|
5021
|
+
isOpen,
|
|
5022
|
+
onClose,
|
|
5023
|
+
title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
|
|
5024
|
+
subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
|
|
5025
|
+
icon: isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
|
|
5026
|
+
maxWidth: "md",
|
|
5027
|
+
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 }),
|
|
5028
|
+
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
5029
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
5113
5030
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
5114
5031
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5227,10 +5144,9 @@ function SaveSlotModal({
|
|
|
5227
5144
|
},
|
|
5228
5145
|
slotNum
|
|
5229
5146
|
);
|
|
5230
|
-
}) })
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
] });
|
|
5147
|
+
}) })
|
|
5148
|
+
}
|
|
5149
|
+
);
|
|
5234
5150
|
}
|
|
5235
5151
|
function BiosSelectionModal({
|
|
5236
5152
|
isOpen,
|
|
@@ -5377,36 +5293,28 @@ function SettingsModal({
|
|
|
5377
5293
|
systemColor = "#00FF41"
|
|
5378
5294
|
}) {
|
|
5379
5295
|
const t = useKoinTranslation();
|
|
5380
|
-
if (!isOpen) return null;
|
|
5381
5296
|
const languages = [
|
|
5382
5297
|
{ code: "en", name: "English" },
|
|
5383
5298
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5384
5299
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5385
5300
|
];
|
|
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: [
|
|
5301
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5302
|
+
ModalShell,
|
|
5303
|
+
{
|
|
5304
|
+
isOpen,
|
|
5305
|
+
onClose,
|
|
5306
|
+
title: t.settings.title,
|
|
5307
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Settings, { size: 20, className: "text-white" }),
|
|
5308
|
+
maxWidth: "sm",
|
|
5309
|
+
footer: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5310
|
+
"button",
|
|
5311
|
+
{
|
|
5312
|
+
onClick: onClose,
|
|
5313
|
+
className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
|
|
5314
|
+
children: t.modals.shortcuts.pressEsc
|
|
5315
|
+
}
|
|
5316
|
+
),
|
|
5317
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5410
5318
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5411
5319
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
|
|
5412
5320
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
|
|
@@ -5418,9 +5326,9 @@ function SettingsModal({
|
|
|
5418
5326
|
{
|
|
5419
5327
|
onClick: () => onLanguageChange(lang.code),
|
|
5420
5328
|
className: `
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5329
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5330
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5331
|
+
`,
|
|
5424
5332
|
children: [
|
|
5425
5333
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
|
|
5426
5334
|
isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5429,17 +5337,9 @@ function SettingsModal({
|
|
|
5429
5337
|
lang.code
|
|
5430
5338
|
);
|
|
5431
5339
|
}) })
|
|
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
|
-
] });
|
|
5340
|
+
] }) })
|
|
5341
|
+
}
|
|
5342
|
+
);
|
|
5443
5343
|
}
|
|
5444
5344
|
function GameModals({
|
|
5445
5345
|
controlsModalOpen,
|
|
@@ -7650,6 +7550,171 @@ var useNostalgist = ({
|
|
|
7650
7550
|
]);
|
|
7651
7551
|
return hookReturn;
|
|
7652
7552
|
};
|
|
7553
|
+
function getDisplayName(id) {
|
|
7554
|
+
let name = id;
|
|
7555
|
+
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
7556
|
+
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
7557
|
+
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
7558
|
+
if (/xbox/i.test(name)) {
|
|
7559
|
+
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
7560
|
+
if (/one/i.test(name)) return "Xbox One Controller";
|
|
7561
|
+
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
7562
|
+
return "Xbox Controller";
|
|
7563
|
+
}
|
|
7564
|
+
if (/dualsense/i.test(name)) return "DualSense";
|
|
7565
|
+
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
7566
|
+
if (/dualshock/i.test(name)) return "DualShock";
|
|
7567
|
+
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
7568
|
+
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
7569
|
+
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
7570
|
+
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
7571
|
+
return name.trim() || "Gamepad";
|
|
7572
|
+
}
|
|
7573
|
+
function detectControllerBrand(id) {
|
|
7574
|
+
const lowerId = id.toLowerCase();
|
|
7575
|
+
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
7576
|
+
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
7577
|
+
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
7578
|
+
return "generic";
|
|
7579
|
+
}
|
|
7580
|
+
function toGamepadInfo(gamepad) {
|
|
7581
|
+
return {
|
|
7582
|
+
index: gamepad.index,
|
|
7583
|
+
id: gamepad.id,
|
|
7584
|
+
name: getDisplayName(gamepad.id),
|
|
7585
|
+
connected: gamepad.connected,
|
|
7586
|
+
buttons: gamepad.buttons.length,
|
|
7587
|
+
axes: gamepad.axes.length,
|
|
7588
|
+
mapping: gamepad.mapping
|
|
7589
|
+
};
|
|
7590
|
+
}
|
|
7591
|
+
function useGamepad(options) {
|
|
7592
|
+
const { onConnect, onDisconnect } = options || {};
|
|
7593
|
+
const [gamepads, setGamepads] = React2.useState([]);
|
|
7594
|
+
const rafRef = React2.useRef(null);
|
|
7595
|
+
const lastStateRef = React2.useRef("");
|
|
7596
|
+
const prevCountRef = React2.useRef(0);
|
|
7597
|
+
const onConnectRef = React2.useRef(onConnect);
|
|
7598
|
+
const onDisconnectRef = React2.useRef(onDisconnect);
|
|
7599
|
+
React2.useEffect(() => {
|
|
7600
|
+
onConnectRef.current = onConnect;
|
|
7601
|
+
onDisconnectRef.current = onDisconnect;
|
|
7602
|
+
}, [onConnect, onDisconnect]);
|
|
7603
|
+
const getGamepads = React2.useCallback(() => {
|
|
7604
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
7605
|
+
return [];
|
|
7606
|
+
}
|
|
7607
|
+
const rawGamepads = navigator.getGamepads() ?? [];
|
|
7608
|
+
const connected = [];
|
|
7609
|
+
for (let i = 0; i < rawGamepads.length; i++) {
|
|
7610
|
+
const gp = rawGamepads[i];
|
|
7611
|
+
if (gp && gp.connected) {
|
|
7612
|
+
connected.push(toGamepadInfo(gp));
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
return connected;
|
|
7616
|
+
}, []);
|
|
7617
|
+
const getRawGamepad = React2.useCallback((index) => {
|
|
7618
|
+
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
7619
|
+
return rawGamepads[index] ?? null;
|
|
7620
|
+
}, []);
|
|
7621
|
+
const refresh = React2.useCallback(() => {
|
|
7622
|
+
setGamepads(getGamepads());
|
|
7623
|
+
}, [getGamepads]);
|
|
7624
|
+
React2.useEffect(() => {
|
|
7625
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
7628
|
+
if (typeof navigator.getGamepads !== "function") {
|
|
7629
|
+
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
7630
|
+
return;
|
|
7631
|
+
}
|
|
7632
|
+
let isActive = true;
|
|
7633
|
+
const poll = () => {
|
|
7634
|
+
if (!isActive) return;
|
|
7635
|
+
const current = getGamepads();
|
|
7636
|
+
let hasChanged = current.length !== prevCountRef.current;
|
|
7637
|
+
if (!hasChanged) {
|
|
7638
|
+
for (let i = 0; i < current.length; i++) {
|
|
7639
|
+
const saved = gamepads[i];
|
|
7640
|
+
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
7641
|
+
hasChanged = true;
|
|
7642
|
+
break;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
if (hasChanged) {
|
|
7647
|
+
const prevCount = prevCountRef.current;
|
|
7648
|
+
const currentCount = current.length;
|
|
7649
|
+
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
7650
|
+
const newGamepad = current[current.length - 1];
|
|
7651
|
+
onConnectRef.current(newGamepad);
|
|
7652
|
+
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
7653
|
+
onDisconnectRef.current();
|
|
7654
|
+
}
|
|
7655
|
+
prevCountRef.current = currentCount;
|
|
7656
|
+
setGamepads(current);
|
|
7657
|
+
}
|
|
7658
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7659
|
+
};
|
|
7660
|
+
const handleConnect = (e) => {
|
|
7661
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
7662
|
+
const current = getGamepads();
|
|
7663
|
+
const prevCount = prevCountRef.current;
|
|
7664
|
+
prevCountRef.current = current.length;
|
|
7665
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7666
|
+
setGamepads(current);
|
|
7667
|
+
if (onConnectRef.current && current.length > prevCount) {
|
|
7668
|
+
const newGamepad = current[current.length - 1];
|
|
7669
|
+
onConnectRef.current(newGamepad);
|
|
7670
|
+
}
|
|
7671
|
+
};
|
|
7672
|
+
const handleDisconnect = (e) => {
|
|
7673
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
7674
|
+
const current = getGamepads();
|
|
7675
|
+
const prevCount = prevCountRef.current;
|
|
7676
|
+
prevCountRef.current = current.length;
|
|
7677
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7678
|
+
setGamepads(current);
|
|
7679
|
+
if (onDisconnectRef.current && current.length < prevCount) {
|
|
7680
|
+
onDisconnectRef.current();
|
|
7681
|
+
}
|
|
7682
|
+
};
|
|
7683
|
+
window.addEventListener("gamepadconnected", handleConnect);
|
|
7684
|
+
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
7685
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7686
|
+
const initial = getGamepads();
|
|
7687
|
+
if (initial.length > 0) {
|
|
7688
|
+
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
7689
|
+
prevCountRef.current = initial.length;
|
|
7690
|
+
setGamepads(initial);
|
|
7691
|
+
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7692
|
+
} else {
|
|
7693
|
+
prevCountRef.current = 0;
|
|
7694
|
+
}
|
|
7695
|
+
return () => {
|
|
7696
|
+
isActive = false;
|
|
7697
|
+
if (rafRef.current) {
|
|
7698
|
+
cancelAnimationFrame(rafRef.current);
|
|
7699
|
+
}
|
|
7700
|
+
window.removeEventListener("gamepadconnected", handleConnect);
|
|
7701
|
+
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
7702
|
+
};
|
|
7703
|
+
}, [getGamepads]);
|
|
7704
|
+
return {
|
|
7705
|
+
gamepads,
|
|
7706
|
+
isAnyConnected: gamepads.length > 0,
|
|
7707
|
+
connectedCount: gamepads.length,
|
|
7708
|
+
getRawGamepad,
|
|
7709
|
+
refresh
|
|
7710
|
+
};
|
|
7711
|
+
}
|
|
7712
|
+
var STANDARD_AXIS_MAP = {
|
|
7713
|
+
leftStickX: 0,
|
|
7714
|
+
leftStickY: 1,
|
|
7715
|
+
rightStickX: 2,
|
|
7716
|
+
rightStickY: 3
|
|
7717
|
+
};
|
|
7653
7718
|
function useVolume({
|
|
7654
7719
|
setVolume: setVolumeInHook,
|
|
7655
7720
|
toggleMute: toggleMuteInHook
|
|
@@ -9537,27 +9602,15 @@ function AchievementPopup({
|
|
|
9537
9602
|
onDismiss,
|
|
9538
9603
|
autoDismissMs = 5e3
|
|
9539
9604
|
}) {
|
|
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
|
-
};
|
|
9605
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9606
|
+
exitDuration: 300,
|
|
9607
|
+
onExit: onDismiss,
|
|
9608
|
+
autoDismissMs
|
|
9609
|
+
});
|
|
9557
9610
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9558
9611
|
"div",
|
|
9559
9612
|
{
|
|
9560
|
-
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${
|
|
9613
|
+
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
|
|
9561
9614
|
children: [
|
|
9562
9615
|
/* @__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
9616
|
/* @__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 +9647,7 @@ function AchievementPopup({
|
|
|
9594
9647
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9595
9648
|
"button",
|
|
9596
9649
|
{
|
|
9597
|
-
onClick:
|
|
9650
|
+
onClick: triggerExit,
|
|
9598
9651
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9599
9652
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18 })
|
|
9600
9653
|
}
|