koin.js 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +918 -758
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +919 -759
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.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
|
});
|
|
@@ -2393,26 +2441,49 @@ function getLayoutForSystem(system) {
|
|
|
2393
2441
|
if (s.includes("ATARI")) return TWO_BUTTON_LAYOUT;
|
|
2394
2442
|
return TWO_BUTTON_LAYOUT;
|
|
2395
2443
|
}
|
|
2396
|
-
|
|
2444
|
+
|
|
2445
|
+
// src/components/VirtualController/utils/dragConstraints.ts
|
|
2446
|
+
function constrainToViewport({
|
|
2447
|
+
newXPercent,
|
|
2448
|
+
newYPercent,
|
|
2449
|
+
elementSize,
|
|
2450
|
+
containerWidth,
|
|
2451
|
+
containerHeight
|
|
2452
|
+
}) {
|
|
2453
|
+
const xMargin = elementSize / 2 / containerWidth * 100;
|
|
2454
|
+
const yMargin = elementSize / 2 / containerHeight * 100;
|
|
2455
|
+
return {
|
|
2456
|
+
x: Math.max(xMargin, Math.min(100 - xMargin, newXPercent)),
|
|
2457
|
+
y: Math.max(yMargin, Math.min(100 - yMargin, newYPercent))
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/components/VirtualController/hooks/useDrag.ts
|
|
2462
|
+
var DEFAULT_HOLD_DELAY = 350;
|
|
2463
|
+
var DEFAULT_CENTER_THRESHOLD = 0.4;
|
|
2397
2464
|
var DRAG_MOVE_THRESHOLD = 10;
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
buttonType,
|
|
2401
|
-
isSystemButton,
|
|
2402
|
-
buttonSize,
|
|
2465
|
+
function useDrag({
|
|
2466
|
+
elementSize,
|
|
2403
2467
|
displayX,
|
|
2404
2468
|
displayY,
|
|
2405
2469
|
containerWidth,
|
|
2406
2470
|
containerHeight,
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2471
|
+
onPositionChange,
|
|
2472
|
+
holdDelay = DEFAULT_HOLD_DELAY,
|
|
2473
|
+
centerThreshold = DEFAULT_CENTER_THRESHOLD,
|
|
2474
|
+
onDragStart,
|
|
2475
|
+
onDragEnd
|
|
2411
2476
|
}) {
|
|
2477
|
+
const [isDragging, setIsDragging] = React2.useState(false);
|
|
2412
2478
|
const isDraggingRef = React2.useRef(false);
|
|
2413
|
-
const dragStartRef = React2.useRef({ x: 0, y: 0 });
|
|
2414
2479
|
const dragTimerRef = React2.useRef(null);
|
|
2415
2480
|
const touchStartPosRef = React2.useRef({ x: 0, y: 0 });
|
|
2481
|
+
const dragStartRef = React2.useRef({
|
|
2482
|
+
elementX: 0,
|
|
2483
|
+
elementY: 0,
|
|
2484
|
+
touchX: 0,
|
|
2485
|
+
touchY: 0
|
|
2486
|
+
});
|
|
2416
2487
|
const clearDragTimer = React2.useCallback(() => {
|
|
2417
2488
|
if (dragTimerRef.current) {
|
|
2418
2489
|
clearTimeout(dragTimerRef.current);
|
|
@@ -2422,23 +2493,129 @@ function useTouchHandlers({
|
|
|
2422
2493
|
const startDragging = React2.useCallback(
|
|
2423
2494
|
(touchX, touchY) => {
|
|
2424
2495
|
isDraggingRef.current = true;
|
|
2496
|
+
setIsDragging(true);
|
|
2425
2497
|
dragStartRef.current = {
|
|
2426
|
-
|
|
2427
|
-
|
|
2498
|
+
elementX: displayX,
|
|
2499
|
+
elementY: displayY,
|
|
2500
|
+
touchX,
|
|
2501
|
+
touchY
|
|
2428
2502
|
};
|
|
2429
2503
|
if (navigator.vibrate) {
|
|
2430
2504
|
navigator.vibrate([10, 30, 10]);
|
|
2431
2505
|
}
|
|
2506
|
+
onDragStart?.();
|
|
2507
|
+
},
|
|
2508
|
+
[displayX, displayY, onDragStart]
|
|
2509
|
+
);
|
|
2510
|
+
const checkDragStart = React2.useCallback(
|
|
2511
|
+
(touchX, touchY, elementRect) => {
|
|
2512
|
+
if (!onPositionChange) return false;
|
|
2513
|
+
touchStartPosRef.current = { x: touchX, y: touchY };
|
|
2514
|
+
const centerX = elementRect.left + elementRect.width / 2;
|
|
2515
|
+
const centerY = elementRect.top + elementRect.height / 2;
|
|
2516
|
+
const distFromCenter = Math.sqrt(
|
|
2517
|
+
Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2)
|
|
2518
|
+
);
|
|
2519
|
+
const centerRadius = elementSize * centerThreshold;
|
|
2520
|
+
if (distFromCenter < centerRadius) {
|
|
2521
|
+
dragTimerRef.current = setTimeout(() => {
|
|
2522
|
+
if (!isDraggingRef.current) {
|
|
2523
|
+
startDragging(touchX, touchY);
|
|
2524
|
+
}
|
|
2525
|
+
}, holdDelay);
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
return false;
|
|
2529
|
+
},
|
|
2530
|
+
[onPositionChange, elementSize, centerThreshold, holdDelay, startDragging]
|
|
2531
|
+
);
|
|
2532
|
+
const checkMoveThreshold = React2.useCallback(
|
|
2533
|
+
(touchX, touchY) => {
|
|
2534
|
+
if (!onPositionChange || isDraggingRef.current) return false;
|
|
2535
|
+
const moveDistance = Math.sqrt(
|
|
2536
|
+
Math.pow(touchX - touchStartPosRef.current.x, 2) + Math.pow(touchY - touchStartPosRef.current.y, 2)
|
|
2537
|
+
);
|
|
2538
|
+
if (moveDistance > DRAG_MOVE_THRESHOLD) {
|
|
2539
|
+
clearDragTimer();
|
|
2540
|
+
startDragging(touchX, touchY);
|
|
2541
|
+
return true;
|
|
2542
|
+
}
|
|
2543
|
+
return false;
|
|
2544
|
+
},
|
|
2545
|
+
[onPositionChange, clearDragTimer, startDragging]
|
|
2546
|
+
);
|
|
2547
|
+
const handleDragMove = React2.useCallback(
|
|
2548
|
+
(touchX, touchY) => {
|
|
2549
|
+
if (!isDraggingRef.current || !onPositionChange) return;
|
|
2550
|
+
const deltaX = touchX - dragStartRef.current.touchX;
|
|
2551
|
+
const deltaY = touchY - dragStartRef.current.touchY;
|
|
2552
|
+
const newXPercent = dragStartRef.current.elementX + deltaX / containerWidth * 100;
|
|
2553
|
+
const newYPercent = dragStartRef.current.elementY + deltaY / containerHeight * 100;
|
|
2554
|
+
const constrained = constrainToViewport({
|
|
2555
|
+
newXPercent,
|
|
2556
|
+
newYPercent,
|
|
2557
|
+
elementSize,
|
|
2558
|
+
containerWidth,
|
|
2559
|
+
containerHeight
|
|
2560
|
+
});
|
|
2561
|
+
onPositionChange(constrained.x, constrained.y);
|
|
2562
|
+
},
|
|
2563
|
+
[onPositionChange, containerWidth, containerHeight, elementSize]
|
|
2564
|
+
);
|
|
2565
|
+
const handleDragEnd = React2.useCallback(() => {
|
|
2566
|
+
clearDragTimer();
|
|
2567
|
+
if (isDraggingRef.current) {
|
|
2568
|
+
isDraggingRef.current = false;
|
|
2569
|
+
setIsDragging(false);
|
|
2570
|
+
onDragEnd?.();
|
|
2571
|
+
}
|
|
2572
|
+
}, [clearDragTimer, onDragEnd]);
|
|
2573
|
+
return {
|
|
2574
|
+
isDragging,
|
|
2575
|
+
checkDragStart,
|
|
2576
|
+
handleDragMove,
|
|
2577
|
+
handleDragEnd,
|
|
2578
|
+
clearDragTimer,
|
|
2579
|
+
checkMoveThreshold
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/components/VirtualController/hooks/useTouchHandlers.ts
|
|
2584
|
+
function useTouchHandlers({
|
|
2585
|
+
buttonType,
|
|
2586
|
+
isSystemButton,
|
|
2587
|
+
buttonSize,
|
|
2588
|
+
displayX,
|
|
2589
|
+
displayY,
|
|
2590
|
+
containerWidth,
|
|
2591
|
+
containerHeight,
|
|
2592
|
+
onPress,
|
|
2593
|
+
onPressDown,
|
|
2594
|
+
onRelease,
|
|
2595
|
+
onPositionChange
|
|
2596
|
+
}) {
|
|
2597
|
+
const isDraggingRef = React2.useRef(false);
|
|
2598
|
+
const drag = useDrag({
|
|
2599
|
+
elementSize: buttonSize,
|
|
2600
|
+
displayX,
|
|
2601
|
+
displayY,
|
|
2602
|
+
containerWidth,
|
|
2603
|
+
containerHeight,
|
|
2604
|
+
onPositionChange,
|
|
2605
|
+
centerThreshold: 0.4,
|
|
2606
|
+
onDragStart: () => {
|
|
2607
|
+
isDraggingRef.current = true;
|
|
2432
2608
|
if (!isSystemButton) {
|
|
2433
2609
|
onRelease(buttonType);
|
|
2434
2610
|
}
|
|
2435
2611
|
},
|
|
2436
|
-
|
|
2437
|
-
|
|
2612
|
+
onDragEnd: () => {
|
|
2613
|
+
isDraggingRef.current = false;
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2438
2616
|
const handleTouchStart = React2.useCallback(
|
|
2439
2617
|
(e) => {
|
|
2440
2618
|
const touch = e.touches[0];
|
|
2441
|
-
touchStartPosRef.current = { x: touch.clientX, y: touch.clientY };
|
|
2442
2619
|
e.preventDefault();
|
|
2443
2620
|
e.stopPropagation();
|
|
2444
2621
|
if (navigator.vibrate) {
|
|
@@ -2453,57 +2630,32 @@ function useTouchHandlers({
|
|
|
2453
2630
|
const target = e.currentTarget;
|
|
2454
2631
|
if (!target) return;
|
|
2455
2632
|
const rect = target.getBoundingClientRect();
|
|
2456
|
-
|
|
2457
|
-
const centerY = rect.top + rect.height / 2;
|
|
2458
|
-
const distance = Math.sqrt(
|
|
2459
|
-
Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
|
|
2460
|
-
);
|
|
2461
|
-
const dragThreshold = buttonSize * DRAG_CENTER_THRESHOLD;
|
|
2462
|
-
if (distance < dragThreshold) {
|
|
2463
|
-
dragTimerRef.current = setTimeout(() => {
|
|
2464
|
-
if (!isDraggingRef.current) {
|
|
2465
|
-
startDragging(touch.clientX, touch.clientY);
|
|
2466
|
-
}
|
|
2467
|
-
}, DRAG_HOLD_DELAY);
|
|
2468
|
-
}
|
|
2633
|
+
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
2469
2634
|
}
|
|
2470
2635
|
},
|
|
2471
|
-
[isSystemButton, buttonType, onPress, onPressDown, onPositionChange,
|
|
2636
|
+
[isSystemButton, buttonType, onPress, onPressDown, onPositionChange, drag]
|
|
2472
2637
|
);
|
|
2473
2638
|
const handleTouchMove = React2.useCallback(
|
|
2474
2639
|
(e) => {
|
|
2475
2640
|
const touch = e.touches[0];
|
|
2476
2641
|
if (onPositionChange && !isDraggingRef.current) {
|
|
2477
|
-
|
|
2478
|
-
Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
|
|
2479
|
-
);
|
|
2480
|
-
if (moveDistance > DRAG_MOVE_THRESHOLD) {
|
|
2481
|
-
clearDragTimer();
|
|
2482
|
-
startDragging(touch.clientX, touch.clientY);
|
|
2483
|
-
}
|
|
2642
|
+
drag.checkMoveThreshold(touch.clientX, touch.clientY);
|
|
2484
2643
|
}
|
|
2485
|
-
if (isDraggingRef.current
|
|
2644
|
+
if (isDraggingRef.current) {
|
|
2486
2645
|
e.preventDefault();
|
|
2487
2646
|
e.stopPropagation();
|
|
2488
|
-
|
|
2489
|
-
const newY = touch.clientY - dragStartRef.current.y;
|
|
2490
|
-
const newXPercent = newX / containerWidth * 100;
|
|
2491
|
-
const newYPercent = newY / containerHeight * 100;
|
|
2492
|
-
const margin = buttonSize / 2 / Math.min(containerWidth, containerHeight) * 100;
|
|
2493
|
-
const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
|
|
2494
|
-
const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
|
|
2495
|
-
onPositionChange(constrainedX, constrainedY);
|
|
2647
|
+
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
2496
2648
|
}
|
|
2497
2649
|
},
|
|
2498
|
-
[onPositionChange,
|
|
2650
|
+
[onPositionChange, drag]
|
|
2499
2651
|
);
|
|
2500
2652
|
const handleTouchEnd = React2.useCallback(
|
|
2501
2653
|
(e) => {
|
|
2502
|
-
clearDragTimer();
|
|
2654
|
+
drag.clearDragTimer();
|
|
2503
2655
|
if (isDraggingRef.current) {
|
|
2504
2656
|
e.preventDefault();
|
|
2505
2657
|
e.stopPropagation();
|
|
2506
|
-
|
|
2658
|
+
drag.handleDragEnd();
|
|
2507
2659
|
return;
|
|
2508
2660
|
}
|
|
2509
2661
|
e.preventDefault();
|
|
@@ -2512,15 +2664,15 @@ function useTouchHandlers({
|
|
|
2512
2664
|
onRelease(buttonType);
|
|
2513
2665
|
}
|
|
2514
2666
|
},
|
|
2515
|
-
[
|
|
2667
|
+
[drag, isSystemButton, buttonType, onRelease]
|
|
2516
2668
|
);
|
|
2517
2669
|
const handleTouchCancel = React2.useCallback(
|
|
2518
2670
|
(e) => {
|
|
2519
|
-
clearDragTimer();
|
|
2671
|
+
drag.clearDragTimer();
|
|
2520
2672
|
if (isDraggingRef.current) {
|
|
2521
2673
|
e.preventDefault();
|
|
2522
2674
|
e.stopPropagation();
|
|
2523
|
-
|
|
2675
|
+
drag.handleDragEnd();
|
|
2524
2676
|
return;
|
|
2525
2677
|
}
|
|
2526
2678
|
e.preventDefault();
|
|
@@ -2529,11 +2681,11 @@ function useTouchHandlers({
|
|
|
2529
2681
|
onRelease(buttonType);
|
|
2530
2682
|
}
|
|
2531
2683
|
},
|
|
2532
|
-
[
|
|
2684
|
+
[drag, isSystemButton, buttonType, onRelease]
|
|
2533
2685
|
);
|
|
2534
2686
|
const cleanup = React2.useCallback(() => {
|
|
2535
|
-
clearDragTimer();
|
|
2536
|
-
}, [
|
|
2687
|
+
drag.clearDragTimer();
|
|
2688
|
+
}, [drag]);
|
|
2537
2689
|
return {
|
|
2538
2690
|
handleTouchStart,
|
|
2539
2691
|
handleTouchMove,
|
|
@@ -2542,6 +2694,39 @@ function useTouchHandlers({
|
|
|
2542
2694
|
cleanup
|
|
2543
2695
|
};
|
|
2544
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
|
+
}
|
|
2545
2730
|
|
|
2546
2731
|
// src/components/VirtualController/utils/buttonStyles.ts
|
|
2547
2732
|
var DEFAULT_FACE = {
|
|
@@ -2699,21 +2884,12 @@ var VirtualButton = React2__default.default.memo(function VirtualButton2({
|
|
|
2699
2884
|
onRelease,
|
|
2700
2885
|
onPositionChange
|
|
2701
2886
|
});
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
|
|
2709
|
-
return () => {
|
|
2710
|
-
button.removeEventListener("touchstart", handleTouchStart);
|
|
2711
|
-
button.removeEventListener("touchmove", handleTouchMove);
|
|
2712
|
-
button.removeEventListener("touchend", handleTouchEnd);
|
|
2713
|
-
button.removeEventListener("touchcancel", handleTouchCancel);
|
|
2714
|
-
cleanup();
|
|
2715
|
-
};
|
|
2716
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
|
|
2887
|
+
useTouchEvents(buttonRef, {
|
|
2888
|
+
onTouchStart: handleTouchStart,
|
|
2889
|
+
onTouchMove: handleTouchMove,
|
|
2890
|
+
onTouchEnd: handleTouchEnd,
|
|
2891
|
+
onTouchCancel: handleTouchCancel
|
|
2892
|
+
}, { cleanup });
|
|
2717
2893
|
const leftPercent = displayX / 100 * containerWidth - config.size / 2;
|
|
2718
2894
|
const topPercent = displayY / 100 * containerHeight - config.size / 2;
|
|
2719
2895
|
const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
|
|
@@ -3389,8 +3565,7 @@ function dispatchKeyboardEvent(type, code) {
|
|
|
3389
3565
|
canvas.dispatchEvent(event);
|
|
3390
3566
|
return true;
|
|
3391
3567
|
}
|
|
3392
|
-
var
|
|
3393
|
-
var CENTER_TOUCH_RADIUS = 0.25;
|
|
3568
|
+
var CENTER_TOUCH_RADIUS = 0.35;
|
|
3394
3569
|
var Dpad = React2__default.default.memo(function Dpad2({
|
|
3395
3570
|
size = 180,
|
|
3396
3571
|
x,
|
|
@@ -3406,10 +3581,6 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3406
3581
|
const dpadRef = React2.useRef(null);
|
|
3407
3582
|
const activeTouchRef = React2.useRef(null);
|
|
3408
3583
|
const activeDirectionsRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
3409
|
-
const [isDragging, setIsDragging] = React2.useState(false);
|
|
3410
|
-
const dragTimerRef = React2.useRef(null);
|
|
3411
|
-
const dragStartRef = React2.useRef({ x: 0, y: 0, touchX: 0, touchY: 0 });
|
|
3412
|
-
const touchStartPosRef = React2.useRef({ x: 0, y: 0, time: 0 });
|
|
3413
3584
|
const upPathRef = React2.useRef(null);
|
|
3414
3585
|
const downPathRef = React2.useRef(null);
|
|
3415
3586
|
const leftPathRef = React2.useRef(null);
|
|
@@ -3417,6 +3588,13 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3417
3588
|
const centerCircleRef = React2.useRef(null);
|
|
3418
3589
|
const displayX = customPosition ? customPosition.x : x;
|
|
3419
3590
|
const displayY = customPosition ? customPosition.y : y;
|
|
3591
|
+
const releaseAllDirections = React2.useCallback((getKeyCode2) => {
|
|
3592
|
+
activeDirectionsRef.current.forEach((dir) => {
|
|
3593
|
+
const keyCode = getKeyCode2(dir);
|
|
3594
|
+
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3595
|
+
});
|
|
3596
|
+
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3597
|
+
}, []);
|
|
3420
3598
|
const getKeyCode = React2.useCallback((direction) => {
|
|
3421
3599
|
if (!controls) {
|
|
3422
3600
|
const defaults = {
|
|
@@ -3429,6 +3607,19 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3429
3607
|
}
|
|
3430
3608
|
return controls[direction] || "";
|
|
3431
3609
|
}, [controls]);
|
|
3610
|
+
const drag = useDrag({
|
|
3611
|
+
elementSize: size,
|
|
3612
|
+
displayX,
|
|
3613
|
+
displayY,
|
|
3614
|
+
containerWidth,
|
|
3615
|
+
containerHeight,
|
|
3616
|
+
onPositionChange,
|
|
3617
|
+
centerThreshold: CENTER_TOUCH_RADIUS,
|
|
3618
|
+
onDragStart: () => {
|
|
3619
|
+
releaseAllDirections(getKeyCode);
|
|
3620
|
+
updateVisuals(/* @__PURE__ */ new Set());
|
|
3621
|
+
}
|
|
3622
|
+
});
|
|
3432
3623
|
const getDirectionsFromTouch = React2.useCallback((touchX, touchY, rect) => {
|
|
3433
3624
|
const centerX = rect.left + rect.width / 2;
|
|
3434
3625
|
const centerY = rect.top + rect.height / 2;
|
|
@@ -3490,51 +3681,20 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3490
3681
|
activeDirectionsRef.current = newDirections;
|
|
3491
3682
|
updateVisuals(newDirections);
|
|
3492
3683
|
}, [getKeyCode, updateVisuals]);
|
|
3493
|
-
const clearDragTimer = React2.useCallback(() => {
|
|
3494
|
-
if (dragTimerRef.current) {
|
|
3495
|
-
clearTimeout(dragTimerRef.current);
|
|
3496
|
-
dragTimerRef.current = null;
|
|
3497
|
-
}
|
|
3498
|
-
}, []);
|
|
3499
|
-
const startDragging = React2.useCallback((touchX, touchY) => {
|
|
3500
|
-
setIsDragging(true);
|
|
3501
|
-
dragStartRef.current = {
|
|
3502
|
-
x: displayX,
|
|
3503
|
-
y: displayY,
|
|
3504
|
-
touchX,
|
|
3505
|
-
touchY
|
|
3506
|
-
};
|
|
3507
|
-
activeDirectionsRef.current.forEach((dir) => {
|
|
3508
|
-
const keyCode = getKeyCode(dir);
|
|
3509
|
-
if (keyCode) dispatchKeyboardEvent("keyup", keyCode);
|
|
3510
|
-
});
|
|
3511
|
-
activeDirectionsRef.current = /* @__PURE__ */ new Set();
|
|
3512
|
-
updateVisuals(/* @__PURE__ */ new Set());
|
|
3513
|
-
if (navigator.vibrate) navigator.vibrate([10, 30, 10]);
|
|
3514
|
-
}, [displayX, displayY, getKeyCode, updateVisuals]);
|
|
3515
3684
|
const handleTouchStart = React2.useCallback((e) => {
|
|
3516
3685
|
e.preventDefault();
|
|
3517
3686
|
if (activeTouchRef.current !== null) return;
|
|
3518
3687
|
const touch = e.changedTouches[0];
|
|
3519
3688
|
activeTouchRef.current = touch.identifier;
|
|
3520
|
-
touchStartPosRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
|
|
3521
3689
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3522
3690
|
if (!rect) return;
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
const distFromCenter = Math.sqrt(
|
|
3526
|
-
Math.pow(touch.clientX - centerX, 2) + Math.pow(touch.clientY - centerY, 2)
|
|
3527
|
-
);
|
|
3528
|
-
const centerRadius = size * CENTER_TOUCH_RADIUS;
|
|
3529
|
-
if (distFromCenter < centerRadius && onPositionChange) {
|
|
3530
|
-
dragTimerRef.current = setTimeout(() => {
|
|
3531
|
-
startDragging(touch.clientX, touch.clientY);
|
|
3532
|
-
}, DRAG_HOLD_DELAY2);
|
|
3691
|
+
if (onPositionChange) {
|
|
3692
|
+
drag.checkDragStart(touch.clientX, touch.clientY, rect);
|
|
3533
3693
|
}
|
|
3534
|
-
if (!isDragging) {
|
|
3694
|
+
if (!drag.isDragging) {
|
|
3535
3695
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3536
3696
|
}
|
|
3537
|
-
}, [getDirectionsFromTouch, updateDirections,
|
|
3697
|
+
}, [getDirectionsFromTouch, updateDirections, onPositionChange, drag]);
|
|
3538
3698
|
const handleTouchMove = React2.useCallback((e) => {
|
|
3539
3699
|
e.preventDefault();
|
|
3540
3700
|
let touch = null;
|
|
@@ -3545,31 +3705,19 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3545
3705
|
}
|
|
3546
3706
|
}
|
|
3547
3707
|
if (!touch) return;
|
|
3548
|
-
if (isDragging
|
|
3549
|
-
|
|
3550
|
-
const deltaY = touch.clientY - dragStartRef.current.touchY;
|
|
3551
|
-
const newXPercent = dragStartRef.current.x + deltaX / containerWidth * 100;
|
|
3552
|
-
const newYPercent = dragStartRef.current.y + deltaY / containerHeight * 100;
|
|
3553
|
-
const margin = size / 2 / Math.min(containerWidth, containerHeight) * 100;
|
|
3554
|
-
const constrainedX = Math.max(margin, Math.min(100 - margin, newXPercent));
|
|
3555
|
-
const constrainedY = Math.max(margin, Math.min(100 - margin, newYPercent));
|
|
3556
|
-
onPositionChange(constrainedX, constrainedY);
|
|
3708
|
+
if (drag.isDragging) {
|
|
3709
|
+
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3557
3710
|
} else {
|
|
3558
|
-
const moveDistance = Math.sqrt(
|
|
3559
|
-
Math.pow(touch.clientX - touchStartPosRef.current.x, 2) + Math.pow(touch.clientY - touchStartPosRef.current.y, 2)
|
|
3560
|
-
);
|
|
3561
|
-
if (moveDistance > 15) {
|
|
3562
|
-
clearDragTimer();
|
|
3563
|
-
}
|
|
3564
3711
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3565
3712
|
if (rect) {
|
|
3713
|
+
drag.clearDragTimer();
|
|
3566
3714
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3567
3715
|
}
|
|
3568
3716
|
}
|
|
3569
|
-
}, [
|
|
3717
|
+
}, [drag, getDirectionsFromTouch, updateDirections]);
|
|
3570
3718
|
const handleTouchEnd = React2.useCallback((e) => {
|
|
3571
3719
|
e.preventDefault();
|
|
3572
|
-
clearDragTimer();
|
|
3720
|
+
drag.clearDragTimer();
|
|
3573
3721
|
let touchEnded = false;
|
|
3574
3722
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
3575
3723
|
if (e.changedTouches[i].identifier === activeTouchRef.current) {
|
|
@@ -3579,8 +3727,8 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3579
3727
|
}
|
|
3580
3728
|
if (touchEnded) {
|
|
3581
3729
|
activeTouchRef.current = null;
|
|
3582
|
-
if (isDragging) {
|
|
3583
|
-
|
|
3730
|
+
if (drag.isDragging) {
|
|
3731
|
+
drag.handleDragEnd();
|
|
3584
3732
|
} else {
|
|
3585
3733
|
activeDirectionsRef.current.forEach((dir) => {
|
|
3586
3734
|
const keyCode = getKeyCode(dir);
|
|
@@ -3590,22 +3738,13 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3590
3738
|
updateVisuals(/* @__PURE__ */ new Set());
|
|
3591
3739
|
}
|
|
3592
3740
|
}
|
|
3593
|
-
}, [getKeyCode, updateVisuals,
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
|
|
3601
|
-
return () => {
|
|
3602
|
-
dpad.removeEventListener("touchstart", handleTouchStart);
|
|
3603
|
-
dpad.removeEventListener("touchmove", handleTouchMove);
|
|
3604
|
-
dpad.removeEventListener("touchend", handleTouchEnd);
|
|
3605
|
-
dpad.removeEventListener("touchcancel", handleTouchEnd);
|
|
3606
|
-
clearDragTimer();
|
|
3607
|
-
};
|
|
3608
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, clearDragTimer]);
|
|
3741
|
+
}, [getKeyCode, updateVisuals, drag]);
|
|
3742
|
+
useTouchEvents(dpadRef, {
|
|
3743
|
+
onTouchStart: handleTouchStart,
|
|
3744
|
+
onTouchMove: handleTouchMove,
|
|
3745
|
+
onTouchEnd: handleTouchEnd,
|
|
3746
|
+
onTouchCancel: handleTouchEnd
|
|
3747
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3609
3748
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3610
3749
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3611
3750
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3616,21 +3755,21 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3616
3755
|
"div",
|
|
3617
3756
|
{
|
|
3618
3757
|
ref: dpadRef,
|
|
3619
|
-
className: `absolute pointer-events-auto touch-manipulation select-none ${isDragging ? "opacity-60" : ""}`,
|
|
3758
|
+
className: `absolute pointer-events-auto touch-manipulation select-none ${drag.isDragging ? "opacity-60" : ""}`,
|
|
3620
3759
|
style: {
|
|
3621
3760
|
top: 0,
|
|
3622
3761
|
left: 0,
|
|
3623
|
-
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${isDragging ? " scale(1.05)" : ""}`,
|
|
3762
|
+
transform: `translate3d(${leftPx}px, ${topPx}px, 0)${drag.isDragging ? " scale(1.05)" : ""}`,
|
|
3624
3763
|
width: size,
|
|
3625
3764
|
height: size,
|
|
3626
3765
|
opacity: isLandscape ? 0.75 : 0.9,
|
|
3627
3766
|
WebkitTouchCallout: "none",
|
|
3628
3767
|
WebkitUserSelect: "none",
|
|
3629
3768
|
touchAction: "none",
|
|
3630
|
-
transition: isDragging ? "none" : "transform 0.1s ease-out"
|
|
3769
|
+
transition: drag.isDragging ? "none" : "transform 0.1s ease-out"
|
|
3631
3770
|
},
|
|
3632
3771
|
children: [
|
|
3633
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3772
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `absolute inset-0 rounded-full bg-black/40 backdrop-blur-md border shadow-lg ${drag.isDragging ? "border-white/50 ring-2 ring-white/30" : "border-white/10"}` }),
|
|
3634
3773
|
/* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "100%", height: "100%", viewBox: "0 0 100 100", className: "drop-shadow-xl relative z-10", children: [
|
|
3635
3774
|
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: upPathRef, d: dUp, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
3636
3775
|
/* @__PURE__ */ jsxRuntime.jsx("path", { ref: rightPathRef, d: dRight, fill: "rgba(255,255,255,0.05)", stroke: "rgba(255,255,255,0.2)", strokeWidth: "1", className: "transition-all duration-75" }),
|
|
@@ -3643,9 +3782,9 @@ var Dpad = React2__default.default.memo(function Dpad2({
|
|
|
3643
3782
|
cx: "50",
|
|
3644
3783
|
cy: "50",
|
|
3645
3784
|
r: "12",
|
|
3646
|
-
fill: isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3647
|
-
stroke: isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3648
|
-
strokeWidth: isDragging ? 2 : 1
|
|
3785
|
+
fill: drag.isDragging ? systemColor : "rgba(0,0,0,0.5)",
|
|
3786
|
+
stroke: drag.isDragging ? "#fff" : "rgba(255,255,255,0.3)",
|
|
3787
|
+
strokeWidth: drag.isDragging ? 2 : 1
|
|
3649
3788
|
}
|
|
3650
3789
|
),
|
|
3651
3790
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M 50,15 L 50,25 M 45,20 L 50,15 L 55,20", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", opacity: "0.8", pointerEvents: "none" }),
|
|
@@ -3805,6 +3944,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3805
3944
|
}
|
|
3806
3945
|
);
|
|
3807
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";
|
|
3808
3974
|
function VirtualController({
|
|
3809
3975
|
system,
|
|
3810
3976
|
isRunning,
|
|
@@ -3816,8 +3982,22 @@ function VirtualController({
|
|
|
3816
3982
|
const [pressedButtons, setPressedButtons] = React2.useState(/* @__PURE__ */ new Set());
|
|
3817
3983
|
const [containerSize, setContainerSize] = React2.useState({ width: 0, height: 0 });
|
|
3818
3984
|
const [isFullscreenState, setIsFullscreenState] = React2.useState(false);
|
|
3985
|
+
const [isLocked, setIsLocked] = React2.useState(true);
|
|
3819
3986
|
const { getPosition, savePosition } = useButtonPositions();
|
|
3820
|
-
|
|
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
|
+
}, []);
|
|
4000
|
+
const layout = getLayoutForSystem(system);
|
|
3821
4001
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3822
4002
|
if (isPortrait) {
|
|
3823
4003
|
return btn.showInPortrait;
|
|
@@ -3971,6 +4151,14 @@ function VirtualController({
|
|
|
3971
4151
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
3972
4152
|
style: { touchAction: "none" },
|
|
3973
4153
|
children: [
|
|
4154
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4155
|
+
LockButton,
|
|
4156
|
+
{
|
|
4157
|
+
isLocked,
|
|
4158
|
+
onToggle: toggleLock,
|
|
4159
|
+
systemColor
|
|
4160
|
+
}
|
|
4161
|
+
),
|
|
3974
4162
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3975
4163
|
Dpad_default,
|
|
3976
4164
|
{
|
|
@@ -3983,7 +4171,7 @@ function VirtualController({
|
|
|
3983
4171
|
systemColor,
|
|
3984
4172
|
isLandscape,
|
|
3985
4173
|
customPosition: getPosition("up", isLandscape),
|
|
3986
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4174
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
3987
4175
|
}
|
|
3988
4176
|
),
|
|
3989
4177
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3997,7 +4185,7 @@ function VirtualController({
|
|
|
3997
4185
|
containerWidth: width,
|
|
3998
4186
|
containerHeight: height,
|
|
3999
4187
|
customPosition,
|
|
4000
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4188
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4001
4189
|
isLandscape,
|
|
4002
4190
|
console: layout.console
|
|
4003
4191
|
},
|
|
@@ -4073,6 +4261,45 @@ function FloatingFullscreenButton({ onClick, disabled = false }) {
|
|
|
4073
4261
|
}
|
|
4074
4262
|
);
|
|
4075
4263
|
}
|
|
4264
|
+
function FloatingPauseButton({
|
|
4265
|
+
isPaused,
|
|
4266
|
+
onClick,
|
|
4267
|
+
disabled = false,
|
|
4268
|
+
systemColor = "#00FF41"
|
|
4269
|
+
}) {
|
|
4270
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4271
|
+
"button",
|
|
4272
|
+
{
|
|
4273
|
+
onClick,
|
|
4274
|
+
disabled,
|
|
4275
|
+
className: `
|
|
4276
|
+
fixed top-3 left-3 z-50
|
|
4277
|
+
px-3 py-2 rounded-xl
|
|
4278
|
+
bg-black/80 backdrop-blur-md
|
|
4279
|
+
border-2
|
|
4280
|
+
shadow-xl
|
|
4281
|
+
flex items-center gap-2
|
|
4282
|
+
transition-all duration-300
|
|
4283
|
+
hover:scale-105
|
|
4284
|
+
active:scale-95
|
|
4285
|
+
disabled:opacity-40 disabled:cursor-not-allowed
|
|
4286
|
+
touch-manipulation
|
|
4287
|
+
`,
|
|
4288
|
+
style: {
|
|
4289
|
+
paddingTop: "max(env(safe-area-inset-top, 0px), 8px)",
|
|
4290
|
+
borderColor: isPaused ? systemColor : "rgba(255,255,255,0.3)"
|
|
4291
|
+
},
|
|
4292
|
+
"aria-label": isPaused ? "Resume game" : "Pause game",
|
|
4293
|
+
children: isPaused ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4294
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Play, { size: 16, style: { color: systemColor }, fill: systemColor }),
|
|
4295
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Play" })
|
|
4296
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4297
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pause, { size: 16, className: "text-white/80" }),
|
|
4298
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white text-xs font-bold uppercase tracking-wider", children: "Pause" })
|
|
4299
|
+
] })
|
|
4300
|
+
}
|
|
4301
|
+
);
|
|
4302
|
+
}
|
|
4076
4303
|
function LoadingSpinner({ color, size = "lg" }) {
|
|
4077
4304
|
const sizeClass = size === "lg" ? "w-12 h-12" : "w-8 h-8";
|
|
4078
4305
|
return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: `${sizeClass} animate-spin`, style: { color } });
|
|
@@ -4278,6 +4505,45 @@ var GameCanvas = React2.memo(function GameCanvas2({
|
|
|
4278
4505
|
] });
|
|
4279
4506
|
});
|
|
4280
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
|
+
}
|
|
4281
4547
|
function getFilteredGroups(activeButtons) {
|
|
4282
4548
|
return BUTTON_GROUPS.map((group) => ({
|
|
4283
4549
|
...group,
|
|
@@ -4293,7 +4559,10 @@ function ControlMapper({
|
|
|
4293
4559
|
}) {
|
|
4294
4560
|
const t = useKoinTranslation();
|
|
4295
4561
|
const [localControls, setLocalControls] = React2.useState(controls);
|
|
4296
|
-
const
|
|
4562
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4563
|
+
isOpen,
|
|
4564
|
+
onClose
|
|
4565
|
+
});
|
|
4297
4566
|
const activeButtons = React2.useMemo(() => {
|
|
4298
4567
|
return getConsoleButtons(system || "SNES");
|
|
4299
4568
|
}, [system]);
|
|
@@ -4309,27 +4578,20 @@ function ControlMapper({
|
|
|
4309
4578
|
}
|
|
4310
4579
|
}, [isOpen, controls]);
|
|
4311
4580
|
React2.useEffect(() => {
|
|
4312
|
-
if (!isOpen)
|
|
4313
|
-
setListeningFor(null);
|
|
4314
|
-
return;
|
|
4315
|
-
}
|
|
4581
|
+
if (!isOpen || !listeningFor) return;
|
|
4316
4582
|
const handleKeyDown = (e) => {
|
|
4317
|
-
if (
|
|
4583
|
+
if (e.code === "Escape") return;
|
|
4318
4584
|
e.preventDefault();
|
|
4319
4585
|
e.stopPropagation();
|
|
4320
|
-
if (e.code === "Escape") {
|
|
4321
|
-
setListeningFor(null);
|
|
4322
|
-
return;
|
|
4323
|
-
}
|
|
4324
4586
|
setLocalControls((prev) => ({
|
|
4325
4587
|
...prev,
|
|
4326
4588
|
[listeningFor]: e.code
|
|
4327
4589
|
}));
|
|
4328
|
-
|
|
4590
|
+
stopListening();
|
|
4329
4591
|
};
|
|
4330
4592
|
window.addEventListener("keydown", handleKeyDown);
|
|
4331
4593
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4332
|
-
}, [isOpen, listeningFor]);
|
|
4594
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4333
4595
|
const handleReset = () => {
|
|
4334
4596
|
setLocalControls(defaultControls);
|
|
4335
4597
|
};
|
|
@@ -4337,52 +4599,58 @@ function ControlMapper({
|
|
|
4337
4599
|
onSave(localControls);
|
|
4338
4600
|
onClose();
|
|
4339
4601
|
};
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
}
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
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(
|
|
4359
4624
|
"button",
|
|
4360
4625
|
{
|
|
4361
|
-
onClick:
|
|
4362
|
-
className: "
|
|
4363
|
-
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
|
+
]
|
|
4364
4632
|
}
|
|
4365
4633
|
)
|
|
4366
4634
|
] }),
|
|
4367
|
-
/* @__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: [
|
|
4368
4636
|
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4369
4637
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4370
4638
|
"button",
|
|
4371
4639
|
{
|
|
4372
|
-
onClick: () =>
|
|
4640
|
+
onClick: () => startListening(btn),
|
|
4373
4641
|
className: `
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
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
|
+
`,
|
|
4377
4645
|
children: [
|
|
4378
4646
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4379
4647
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4380
4648
|
"span",
|
|
4381
4649
|
{
|
|
4382
4650
|
className: `
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
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
|
+
`,
|
|
4386
4654
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4387
4655
|
}
|
|
4388
4656
|
)
|
|
@@ -4390,199 +4658,10 @@ function ControlMapper({
|
|
|
4390
4658
|
},
|
|
4391
4659
|
btn
|
|
4392
4660
|
)) })
|
|
4393
|
-
] }, group.label)) })
|
|
4394
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4395
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4396
|
-
"button",
|
|
4397
|
-
{
|
|
4398
|
-
onClick: handleReset,
|
|
4399
|
-
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",
|
|
4400
|
-
children: [
|
|
4401
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { size: 16 }),
|
|
4402
|
-
t.modals.controls.reset
|
|
4403
|
-
]
|
|
4404
|
-
}
|
|
4405
|
-
),
|
|
4406
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4407
|
-
"button",
|
|
4408
|
-
{
|
|
4409
|
-
onClick: handleSave,
|
|
4410
|
-
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",
|
|
4411
|
-
children: [
|
|
4412
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16 }),
|
|
4413
|
-
t.modals.controls.save
|
|
4414
|
-
]
|
|
4415
|
-
}
|
|
4416
|
-
)
|
|
4417
|
-
] })
|
|
4418
|
-
] })
|
|
4419
|
-
] });
|
|
4420
|
-
}
|
|
4421
|
-
function getDisplayName(id) {
|
|
4422
|
-
let name = id;
|
|
4423
|
-
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
4424
|
-
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
4425
|
-
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
4426
|
-
if (/xbox/i.test(name)) {
|
|
4427
|
-
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
4428
|
-
if (/one/i.test(name)) return "Xbox One Controller";
|
|
4429
|
-
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
4430
|
-
return "Xbox Controller";
|
|
4431
|
-
}
|
|
4432
|
-
if (/dualsense/i.test(name)) return "DualSense";
|
|
4433
|
-
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
4434
|
-
if (/dualshock/i.test(name)) return "DualShock";
|
|
4435
|
-
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
4436
|
-
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
4437
|
-
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
4438
|
-
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
4439
|
-
return name.trim() || "Gamepad";
|
|
4440
|
-
}
|
|
4441
|
-
function detectControllerBrand(id) {
|
|
4442
|
-
const lowerId = id.toLowerCase();
|
|
4443
|
-
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
4444
|
-
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
4445
|
-
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
4446
|
-
return "generic";
|
|
4447
|
-
}
|
|
4448
|
-
function toGamepadInfo(gamepad) {
|
|
4449
|
-
return {
|
|
4450
|
-
index: gamepad.index,
|
|
4451
|
-
id: gamepad.id,
|
|
4452
|
-
name: getDisplayName(gamepad.id),
|
|
4453
|
-
connected: gamepad.connected,
|
|
4454
|
-
buttons: gamepad.buttons.length,
|
|
4455
|
-
axes: gamepad.axes.length,
|
|
4456
|
-
mapping: gamepad.mapping
|
|
4457
|
-
};
|
|
4458
|
-
}
|
|
4459
|
-
function useGamepad(options) {
|
|
4460
|
-
const { onConnect, onDisconnect } = options || {};
|
|
4461
|
-
const [gamepads, setGamepads] = React2.useState([]);
|
|
4462
|
-
const rafRef = React2.useRef(null);
|
|
4463
|
-
const lastStateRef = React2.useRef("");
|
|
4464
|
-
const prevCountRef = React2.useRef(0);
|
|
4465
|
-
const onConnectRef = React2.useRef(onConnect);
|
|
4466
|
-
const onDisconnectRef = React2.useRef(onDisconnect);
|
|
4467
|
-
React2.useEffect(() => {
|
|
4468
|
-
onConnectRef.current = onConnect;
|
|
4469
|
-
onDisconnectRef.current = onDisconnect;
|
|
4470
|
-
}, [onConnect, onDisconnect]);
|
|
4471
|
-
const getGamepads = React2.useCallback(() => {
|
|
4472
|
-
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
4473
|
-
return [];
|
|
4474
|
-
}
|
|
4475
|
-
const rawGamepads = navigator.getGamepads() ?? [];
|
|
4476
|
-
const connected = [];
|
|
4477
|
-
for (let i = 0; i < rawGamepads.length; i++) {
|
|
4478
|
-
const gp = rawGamepads[i];
|
|
4479
|
-
if (gp && gp.connected) {
|
|
4480
|
-
connected.push(toGamepadInfo(gp));
|
|
4481
|
-
}
|
|
4482
|
-
}
|
|
4483
|
-
return connected;
|
|
4484
|
-
}, []);
|
|
4485
|
-
const getRawGamepad = React2.useCallback((index) => {
|
|
4486
|
-
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
4487
|
-
return rawGamepads[index] ?? null;
|
|
4488
|
-
}, []);
|
|
4489
|
-
const refresh = React2.useCallback(() => {
|
|
4490
|
-
setGamepads(getGamepads());
|
|
4491
|
-
}, [getGamepads]);
|
|
4492
|
-
React2.useEffect(() => {
|
|
4493
|
-
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
4494
|
-
return;
|
|
4495
|
-
}
|
|
4496
|
-
if (typeof navigator.getGamepads !== "function") {
|
|
4497
|
-
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
4498
|
-
return;
|
|
4661
|
+
] }, group.label)) })
|
|
4499
4662
|
}
|
|
4500
|
-
|
|
4501
|
-
const poll = () => {
|
|
4502
|
-
if (!isActive) return;
|
|
4503
|
-
const current = getGamepads();
|
|
4504
|
-
let hasChanged = current.length !== prevCountRef.current;
|
|
4505
|
-
if (!hasChanged) {
|
|
4506
|
-
for (let i = 0; i < current.length; i++) {
|
|
4507
|
-
const saved = gamepads[i];
|
|
4508
|
-
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
4509
|
-
hasChanged = true;
|
|
4510
|
-
break;
|
|
4511
|
-
}
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
if (hasChanged) {
|
|
4515
|
-
const prevCount = prevCountRef.current;
|
|
4516
|
-
const currentCount = current.length;
|
|
4517
|
-
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
4518
|
-
const newGamepad = current[current.length - 1];
|
|
4519
|
-
onConnectRef.current(newGamepad);
|
|
4520
|
-
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
4521
|
-
onDisconnectRef.current();
|
|
4522
|
-
}
|
|
4523
|
-
prevCountRef.current = currentCount;
|
|
4524
|
-
setGamepads(current);
|
|
4525
|
-
}
|
|
4526
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4527
|
-
};
|
|
4528
|
-
const handleConnect = (e) => {
|
|
4529
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
4530
|
-
const current = getGamepads();
|
|
4531
|
-
const prevCount = prevCountRef.current;
|
|
4532
|
-
prevCountRef.current = current.length;
|
|
4533
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4534
|
-
setGamepads(current);
|
|
4535
|
-
if (onConnectRef.current && current.length > prevCount) {
|
|
4536
|
-
const newGamepad = current[current.length - 1];
|
|
4537
|
-
onConnectRef.current(newGamepad);
|
|
4538
|
-
}
|
|
4539
|
-
};
|
|
4540
|
-
const handleDisconnect = (e) => {
|
|
4541
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
4542
|
-
const current = getGamepads();
|
|
4543
|
-
const prevCount = prevCountRef.current;
|
|
4544
|
-
prevCountRef.current = current.length;
|
|
4545
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4546
|
-
setGamepads(current);
|
|
4547
|
-
if (onDisconnectRef.current && current.length < prevCount) {
|
|
4548
|
-
onDisconnectRef.current();
|
|
4549
|
-
}
|
|
4550
|
-
};
|
|
4551
|
-
window.addEventListener("gamepadconnected", handleConnect);
|
|
4552
|
-
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
4553
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4554
|
-
const initial = getGamepads();
|
|
4555
|
-
if (initial.length > 0) {
|
|
4556
|
-
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
4557
|
-
prevCountRef.current = initial.length;
|
|
4558
|
-
setGamepads(initial);
|
|
4559
|
-
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4560
|
-
} else {
|
|
4561
|
-
prevCountRef.current = 0;
|
|
4562
|
-
}
|
|
4563
|
-
return () => {
|
|
4564
|
-
isActive = false;
|
|
4565
|
-
if (rafRef.current) {
|
|
4566
|
-
cancelAnimationFrame(rafRef.current);
|
|
4567
|
-
}
|
|
4568
|
-
window.removeEventListener("gamepadconnected", handleConnect);
|
|
4569
|
-
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
4570
|
-
};
|
|
4571
|
-
}, [getGamepads]);
|
|
4572
|
-
return {
|
|
4573
|
-
gamepads,
|
|
4574
|
-
isAnyConnected: gamepads.length > 0,
|
|
4575
|
-
connectedCount: gamepads.length,
|
|
4576
|
-
getRawGamepad,
|
|
4577
|
-
refresh
|
|
4578
|
-
};
|
|
4663
|
+
);
|
|
4579
4664
|
}
|
|
4580
|
-
var STANDARD_AXIS_MAP = {
|
|
4581
|
-
leftStickX: 0,
|
|
4582
|
-
leftStickY: 1,
|
|
4583
|
-
rightStickX: 2,
|
|
4584
|
-
rightStickY: 3
|
|
4585
|
-
};
|
|
4586
4665
|
function GamepadMapper({
|
|
4587
4666
|
isOpen,
|
|
4588
4667
|
gamepads,
|
|
@@ -4593,7 +4672,10 @@ function GamepadMapper({
|
|
|
4593
4672
|
const t = useKoinTranslation();
|
|
4594
4673
|
const [selectedPlayer, setSelectedPlayer] = React2.useState(1);
|
|
4595
4674
|
const [bindings, setBindings] = React2.useState({});
|
|
4596
|
-
const
|
|
4675
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4676
|
+
isOpen,
|
|
4677
|
+
onClose
|
|
4678
|
+
});
|
|
4597
4679
|
const rafRef = React2.useRef(null);
|
|
4598
4680
|
React2.useEffect(() => {
|
|
4599
4681
|
if (isOpen) {
|
|
@@ -4628,7 +4710,7 @@ function GamepadMapper({
|
|
|
4628
4710
|
[listeningFor]: i
|
|
4629
4711
|
}
|
|
4630
4712
|
}));
|
|
4631
|
-
|
|
4713
|
+
stopListening();
|
|
4632
4714
|
return;
|
|
4633
4715
|
}
|
|
4634
4716
|
}
|
|
@@ -4641,21 +4723,7 @@ function GamepadMapper({
|
|
|
4641
4723
|
cancelAnimationFrame(rafRef.current);
|
|
4642
4724
|
}
|
|
4643
4725
|
};
|
|
4644
|
-
}, [isOpen, listeningFor, selectedPlayer]);
|
|
4645
|
-
React2.useEffect(() => {
|
|
4646
|
-
if (!isOpen) return;
|
|
4647
|
-
const handleKeyDown = (e) => {
|
|
4648
|
-
if (e.code === "Escape") {
|
|
4649
|
-
if (listeningFor) {
|
|
4650
|
-
setListeningFor(null);
|
|
4651
|
-
} else {
|
|
4652
|
-
onClose();
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
4655
|
-
};
|
|
4656
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
4657
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4658
|
-
}, [isOpen, listeningFor, onClose]);
|
|
4726
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4659
4727
|
const handleReset = () => {
|
|
4660
4728
|
setBindings((prev) => ({
|
|
4661
4729
|
...prev,
|
|
@@ -4670,127 +4738,19 @@ function GamepadMapper({
|
|
|
4670
4738
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4671
4739
|
onClose();
|
|
4672
4740
|
};
|
|
4673
|
-
if (!isOpen) return null;
|
|
4674
4741
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4675
4742
|
const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
}
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4687
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4688
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
|
|
4689
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4690
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
|
|
4691
|
-
/* @__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 })
|
|
4692
|
-
] })
|
|
4693
|
-
] }),
|
|
4694
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4695
|
-
"button",
|
|
4696
|
-
{
|
|
4697
|
-
onClick: onClose,
|
|
4698
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4699
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
4700
|
-
}
|
|
4701
|
-
)
|
|
4702
|
-
] }),
|
|
4703
|
-
gamepads.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4704
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4705
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4706
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4707
|
-
"button",
|
|
4708
|
-
{
|
|
4709
|
-
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4710
|
-
className: `
|
|
4711
|
-
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4712
|
-
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4713
|
-
`,
|
|
4714
|
-
style: selectedPlayer === gp.index + 1 ? {
|
|
4715
|
-
backgroundColor: `${systemColor}20`,
|
|
4716
|
-
color: systemColor
|
|
4717
|
-
} : {},
|
|
4718
|
-
children: [
|
|
4719
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { size: 14 }),
|
|
4720
|
-
"P",
|
|
4721
|
-
gp.index + 1
|
|
4722
|
-
]
|
|
4723
|
-
},
|
|
4724
|
-
gp.index
|
|
4725
|
-
)) })
|
|
4726
|
-
] }),
|
|
4727
|
-
currentGamepad && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4728
|
-
] }),
|
|
4729
|
-
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: [
|
|
4730
|
-
currentGamepad.name,
|
|
4731
|
-
" \u2022 Player 1"
|
|
4732
|
-
] }) }),
|
|
4733
|
-
gamepads.length === 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4734
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4735
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4736
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4737
|
-
] }),
|
|
4738
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4739
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4740
|
-
/* @__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: [
|
|
4741
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4742
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
4743
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4744
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4745
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4746
|
-
] })
|
|
4747
|
-
] })
|
|
4748
|
-
] }),
|
|
4749
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4750
|
-
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: [
|
|
4751
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4752
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4753
|
-
] }),
|
|
4754
|
-
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4755
|
-
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4756
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4757
|
-
"button",
|
|
4758
|
-
{
|
|
4759
|
-
onClick: () => setListeningFor(btn),
|
|
4760
|
-
disabled: !!listeningFor && listeningFor !== btn,
|
|
4761
|
-
className: `
|
|
4762
|
-
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4763
|
-
disabled:opacity-50
|
|
4764
|
-
${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"}
|
|
4765
|
-
`,
|
|
4766
|
-
style: listeningFor === btn ? {
|
|
4767
|
-
borderColor: systemColor,
|
|
4768
|
-
backgroundColor: `${systemColor}20`,
|
|
4769
|
-
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4770
|
-
} : {},
|
|
4771
|
-
children: [
|
|
4772
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4773
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4774
|
-
"span",
|
|
4775
|
-
{
|
|
4776
|
-
className: `
|
|
4777
|
-
px-2 py-1 rounded text-xs font-mono
|
|
4778
|
-
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4779
|
-
`,
|
|
4780
|
-
style: listeningFor === btn ? {
|
|
4781
|
-
backgroundColor: `${systemColor}30`,
|
|
4782
|
-
color: systemColor
|
|
4783
|
-
} : {},
|
|
4784
|
-
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4785
|
-
}
|
|
4786
|
-
)
|
|
4787
|
-
]
|
|
4788
|
-
},
|
|
4789
|
-
btn
|
|
4790
|
-
)) })
|
|
4791
|
-
] }, group.label))
|
|
4792
|
-
] }),
|
|
4793
|
-
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: [
|
|
4794
4754
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4795
4755
|
"button",
|
|
4796
4756
|
{
|
|
@@ -4816,9 +4776,101 @@ function GamepadMapper({
|
|
|
4816
4776
|
]
|
|
4817
4777
|
}
|
|
4818
4778
|
)
|
|
4819
|
-
] })
|
|
4820
|
-
|
|
4821
|
-
|
|
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
|
+
);
|
|
4822
4874
|
}
|
|
4823
4875
|
function CheatModal({
|
|
4824
4876
|
isOpen,
|
|
@@ -4829,39 +4881,21 @@ function CheatModal({
|
|
|
4829
4881
|
}) {
|
|
4830
4882
|
const t = useKoinTranslation();
|
|
4831
4883
|
const [copiedId, setCopiedId] = React2__default.default.useState(null);
|
|
4832
|
-
if (!isOpen) return null;
|
|
4833
4884
|
const handleCopy = async (code, id) => {
|
|
4834
4885
|
await navigator.clipboard.writeText(code);
|
|
4835
|
-
setCopiedId(id);
|
|
4836
|
-
setTimeout(() => setCopiedId(null), 2e3);
|
|
4837
|
-
};
|
|
4838
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4848
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4849
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { className: "text-purple-400", size: 24 }),
|
|
4850
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4851
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
|
|
4852
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
|
|
4853
|
-
] })
|
|
4854
|
-
] }),
|
|
4855
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4856
|
-
"button",
|
|
4857
|
-
{
|
|
4858
|
-
onClick: onClose,
|
|
4859
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4860
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
4861
|
-
}
|
|
4862
|
-
)
|
|
4863
|
-
] }),
|
|
4864
|
-
/* @__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: [
|
|
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: [
|
|
4865
4899
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4866
4900
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4867
4901
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4871,18 +4905,18 @@ function CheatModal({
|
|
|
4871
4905
|
"div",
|
|
4872
4906
|
{
|
|
4873
4907
|
className: `
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
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
|
+
`,
|
|
4877
4911
|
onClick: () => onToggle(cheat.id),
|
|
4878
4912
|
children: [
|
|
4879
4913
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4880
4914
|
"div",
|
|
4881
4915
|
{
|
|
4882
4916
|
className: `
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
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
|
+
`,
|
|
4886
4920
|
children: isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 14, className: "text-white" })
|
|
4887
4921
|
}
|
|
4888
4922
|
),
|
|
@@ -4908,10 +4942,9 @@ function CheatModal({
|
|
|
4908
4942
|
},
|
|
4909
4943
|
cheat.id
|
|
4910
4944
|
);
|
|
4911
|
-
}) })
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
] });
|
|
4945
|
+
}) })
|
|
4946
|
+
}
|
|
4947
|
+
);
|
|
4915
4948
|
}
|
|
4916
4949
|
var AUTO_SAVE_SLOT = 5;
|
|
4917
4950
|
function formatBytes(bytes) {
|
|
@@ -4952,7 +4985,6 @@ function SaveSlotModal({
|
|
|
4952
4985
|
onUpgrade
|
|
4953
4986
|
}) {
|
|
4954
4987
|
const t = useKoinTranslation();
|
|
4955
|
-
if (!isOpen) return null;
|
|
4956
4988
|
const isSaveMode = mode === "save";
|
|
4957
4989
|
const allSlots = [1, 2, 3, 4, 5];
|
|
4958
4990
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -4967,33 +4999,17 @@ function SaveSlotModal({
|
|
|
4967
4999
|
const getSlotData = (slotNum) => {
|
|
4968
5000
|
return slots.find((s) => s.slot === slotNum);
|
|
4969
5001
|
};
|
|
4970
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4980
|
-
|
|
4981
|
-
isSaveMode ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "text-retro-primary", size: 24 }),
|
|
4982
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4983
|
-
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
|
|
4984
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
|
|
4985
|
-
] })
|
|
4986
|
-
] }),
|
|
4987
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4988
|
-
"button",
|
|
4989
|
-
{
|
|
4990
|
-
onClick: onClose,
|
|
4991
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4992
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
4993
|
-
}
|
|
4994
|
-
)
|
|
4995
|
-
] }),
|
|
4996
|
-
/* @__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: [
|
|
4997
5013
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
4998
5014
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
4999
5015
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5112,10 +5128,9 @@ function SaveSlotModal({
|
|
|
5112
5128
|
},
|
|
5113
5129
|
slotNum
|
|
5114
5130
|
);
|
|
5115
|
-
}) })
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
] });
|
|
5131
|
+
}) })
|
|
5132
|
+
}
|
|
5133
|
+
);
|
|
5119
5134
|
}
|
|
5120
5135
|
function BiosSelectionModal({
|
|
5121
5136
|
isOpen,
|
|
@@ -5262,36 +5277,28 @@ function SettingsModal({
|
|
|
5262
5277
|
systemColor = "#00FF41"
|
|
5263
5278
|
}) {
|
|
5264
5279
|
const t = useKoinTranslation();
|
|
5265
|
-
if (!isOpen) return null;
|
|
5266
5280
|
const languages = [
|
|
5267
5281
|
{ code: "en", name: "English" },
|
|
5268
5282
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5269
5283
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5270
5284
|
];
|
|
5271
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
}
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
onClick: onClose,
|
|
5289
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5290
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 20 })
|
|
5291
|
-
}
|
|
5292
|
-
)
|
|
5293
|
-
] }),
|
|
5294
|
-
/* @__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: [
|
|
5295
5302
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5296
5303
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Globe, { size: 16 }),
|
|
5297
5304
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t.settings.language })
|
|
@@ -5303,9 +5310,9 @@ function SettingsModal({
|
|
|
5303
5310
|
{
|
|
5304
5311
|
onClick: () => onLanguageChange(lang.code),
|
|
5305
5312
|
className: `
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
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
|
+
`,
|
|
5309
5316
|
children: [
|
|
5310
5317
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: lang.name }),
|
|
5311
5318
|
isActive && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5314,17 +5321,9 @@ function SettingsModal({
|
|
|
5314
5321
|
lang.code
|
|
5315
5322
|
);
|
|
5316
5323
|
}) })
|
|
5317
|
-
] }) })
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
{
|
|
5321
|
-
onClick: onClose,
|
|
5322
|
-
className: "text-sm text-gray-500 hover:text-white transition-colors",
|
|
5323
|
-
children: t.modals.shortcuts.pressEsc
|
|
5324
|
-
}
|
|
5325
|
-
) })
|
|
5326
|
-
] })
|
|
5327
|
-
] });
|
|
5324
|
+
] }) })
|
|
5325
|
+
}
|
|
5326
|
+
);
|
|
5328
5327
|
}
|
|
5329
5328
|
function GameModals({
|
|
5330
5329
|
controlsModalOpen,
|
|
@@ -7535,6 +7534,171 @@ var useNostalgist = ({
|
|
|
7535
7534
|
]);
|
|
7536
7535
|
return hookReturn;
|
|
7537
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
|
+
};
|
|
7538
7702
|
function useVolume({
|
|
7539
7703
|
setVolume: setVolumeInHook,
|
|
7540
7704
|
toggleMute: toggleMuteInHook
|
|
@@ -9206,6 +9370,14 @@ var GamePlayerInner = React2.memo(function GamePlayerInner2(props) {
|
|
|
9206
9370
|
disabled: status === "loading" || status === "error"
|
|
9207
9371
|
}
|
|
9208
9372
|
),
|
|
9373
|
+
isFullscreen2 && isMobile && (status === "running" || status === "paused") && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9374
|
+
FloatingPauseButton,
|
|
9375
|
+
{
|
|
9376
|
+
isPaused,
|
|
9377
|
+
onClick: handlePauseToggle,
|
|
9378
|
+
systemColor
|
|
9379
|
+
}
|
|
9380
|
+
),
|
|
9209
9381
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 right-2 z-40 flex flex-col items-end gap-2 pointer-events-auto", children: [
|
|
9210
9382
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9211
9383
|
RecordingIndicator_default,
|
|
@@ -9414,27 +9586,15 @@ function AchievementPopup({
|
|
|
9414
9586
|
onDismiss,
|
|
9415
9587
|
autoDismissMs = 5e3
|
|
9416
9588
|
}) {
|
|
9417
|
-
const
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
});
|
|
9423
|
-
const timer = setTimeout(() => {
|
|
9424
|
-
handleDismiss();
|
|
9425
|
-
}, autoDismissMs);
|
|
9426
|
-
return () => clearTimeout(timer);
|
|
9427
|
-
}, [autoDismissMs]);
|
|
9428
|
-
const handleDismiss = () => {
|
|
9429
|
-
setIsExiting(true);
|
|
9430
|
-
setTimeout(() => {
|
|
9431
|
-
onDismiss();
|
|
9432
|
-
}, 300);
|
|
9433
|
-
};
|
|
9589
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9590
|
+
exitDuration: 300,
|
|
9591
|
+
onExit: onDismiss,
|
|
9592
|
+
autoDismissMs
|
|
9593
|
+
});
|
|
9434
9594
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9435
9595
|
"div",
|
|
9436
9596
|
{
|
|
9437
|
-
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}`,
|
|
9438
9598
|
children: [
|
|
9439
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" }),
|
|
9440
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: [
|
|
@@ -9471,7 +9631,7 @@ function AchievementPopup({
|
|
|
9471
9631
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9472
9632
|
"button",
|
|
9473
9633
|
{
|
|
9474
|
-
onClick:
|
|
9634
|
+
onClick: triggerExit,
|
|
9475
9635
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9476
9636
|
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18 })
|
|
9477
9637
|
}
|