koin.js 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +677 -640
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +678 -641
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React2, { memo, useState, useEffect, useRef, createContext, useMemo, useCallback, useContext } from 'react';
|
|
2
|
-
import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard,
|
|
2
|
+
import { Gauge, Play, Pause, RotateCcw, Rewind, Save, Download, Camera, Video, Circle, Monitor, ChevronDown, RefreshCw, HelpCircle, Maximize, Gamepad2, Joystick, Code, Settings, Power, Square, Keyboard, ChevronUp, VolumeX, Volume1, Volume2, Loader2, Trophy, X, AlertTriangle, Minimize2, List, PauseCircle, Check, Clock, Lock, Unlock, Move, User, Copy, Zap, HardDrive, Trash2, Cpu, AlertCircle, FileCode, Globe, ExternalLink, EyeOff, Eye, Shield, CheckCircle, LogOut, Info, XCircle } from 'lucide-react';
|
|
3
3
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { Nostalgist } from 'nostalgist';
|
|
@@ -1441,6 +1441,40 @@ var PlayerControls = memo(function PlayerControls2({
|
|
|
1441
1441
|
] });
|
|
1442
1442
|
});
|
|
1443
1443
|
var PlayerControls_default = PlayerControls;
|
|
1444
|
+
function useAnimatedVisibility({
|
|
1445
|
+
exitDuration = 200,
|
|
1446
|
+
onExit,
|
|
1447
|
+
autoDismissMs
|
|
1448
|
+
} = {}) {
|
|
1449
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
1450
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
1451
|
+
useEffect(() => {
|
|
1452
|
+
requestAnimationFrame(() => {
|
|
1453
|
+
setIsVisible(true);
|
|
1454
|
+
});
|
|
1455
|
+
}, []);
|
|
1456
|
+
useEffect(() => {
|
|
1457
|
+
if (!autoDismissMs) return;
|
|
1458
|
+
const timer = setTimeout(() => {
|
|
1459
|
+
triggerExit();
|
|
1460
|
+
}, autoDismissMs);
|
|
1461
|
+
return () => clearTimeout(timer);
|
|
1462
|
+
}, [autoDismissMs]);
|
|
1463
|
+
const triggerExit = useCallback(() => {
|
|
1464
|
+
if (isExiting) return;
|
|
1465
|
+
setIsExiting(true);
|
|
1466
|
+
setTimeout(() => {
|
|
1467
|
+
onExit?.();
|
|
1468
|
+
}, exitDuration);
|
|
1469
|
+
}, [isExiting, exitDuration, onExit]);
|
|
1470
|
+
const slideInRightClasses = isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0";
|
|
1471
|
+
return {
|
|
1472
|
+
isVisible,
|
|
1473
|
+
isExiting,
|
|
1474
|
+
triggerExit,
|
|
1475
|
+
slideInRightClasses
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1444
1478
|
var TOAST_CONFIGS = {
|
|
1445
1479
|
success: {
|
|
1446
1480
|
icon: CheckCircle,
|
|
@@ -1485,32 +1519,20 @@ var TOAST_CONFIGS = {
|
|
|
1485
1519
|
}
|
|
1486
1520
|
};
|
|
1487
1521
|
function ToastItem({ toast, onDismiss }) {
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1522
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
1523
|
+
exitDuration: 200,
|
|
1524
|
+
onExit: () => onDismiss?.(toast.id)
|
|
1525
|
+
});
|
|
1490
1526
|
const config = TOAST_CONFIGS[toast.type];
|
|
1491
1527
|
const IconComponent = config.icon;
|
|
1492
|
-
useEffect(() => {
|
|
1493
|
-
requestAnimationFrame(() => {
|
|
1494
|
-
setIsVisible(true);
|
|
1495
|
-
});
|
|
1496
|
-
}, []);
|
|
1497
|
-
const handleDismiss = () => {
|
|
1498
|
-
setIsExiting(true);
|
|
1499
|
-
setTimeout(() => {
|
|
1500
|
-
onDismiss?.(toast.id);
|
|
1501
|
-
}, 200);
|
|
1502
|
-
};
|
|
1503
1528
|
return /* @__PURE__ */ jsx(
|
|
1504
1529
|
"div",
|
|
1505
1530
|
{
|
|
1506
|
-
className: `
|
|
1507
|
-
relative transition-all duration-300 ease-out
|
|
1508
|
-
${isVisible && !isExiting ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
|
|
1509
|
-
`,
|
|
1531
|
+
className: `relative transition-all duration-300 ease-out ${slideInRightClasses}`,
|
|
1510
1532
|
children: /* @__PURE__ */ jsxs(
|
|
1511
1533
|
"div",
|
|
1512
1534
|
{
|
|
1513
|
-
className: "relative w-[320px]",
|
|
1535
|
+
className: "relative w-[320px] pointer-events-auto",
|
|
1514
1536
|
style: {
|
|
1515
1537
|
backgroundColor: config.bgColor,
|
|
1516
1538
|
border: `2px solid ${config.borderColor}`,
|
|
@@ -1545,7 +1567,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1545
1567
|
{
|
|
1546
1568
|
onClick: () => {
|
|
1547
1569
|
toast.action?.onClick();
|
|
1548
|
-
|
|
1570
|
+
triggerExit();
|
|
1549
1571
|
},
|
|
1550
1572
|
className: "flex-shrink-0 text-[9px] font-black uppercase tracking-wider px-2 py-1 transition-all hover:-translate-y-0.5 active:translate-y-0",
|
|
1551
1573
|
style: {
|
|
@@ -1559,7 +1581,7 @@ function ToastItem({ toast, onDismiss }) {
|
|
|
1559
1581
|
/* @__PURE__ */ jsx(
|
|
1560
1582
|
"button",
|
|
1561
1583
|
{
|
|
1562
|
-
onClick:
|
|
1584
|
+
onClick: triggerExit,
|
|
1563
1585
|
className: "flex-shrink-0 p-0.5 text-gray-500 hover:text-white transition-colors",
|
|
1564
1586
|
children: /* @__PURE__ */ jsx(X, { size: 14 })
|
|
1565
1587
|
}
|
|
@@ -1932,6 +1954,67 @@ var RecordingIndicator = memo(function RecordingIndicator2({
|
|
|
1932
1954
|
);
|
|
1933
1955
|
});
|
|
1934
1956
|
var RecordingIndicator_default = RecordingIndicator;
|
|
1957
|
+
var MAX_WIDTH_CLASSES = {
|
|
1958
|
+
sm: "max-w-sm",
|
|
1959
|
+
md: "max-w-md",
|
|
1960
|
+
lg: "max-w-lg"
|
|
1961
|
+
};
|
|
1962
|
+
function ModalShell({
|
|
1963
|
+
isOpen,
|
|
1964
|
+
onClose,
|
|
1965
|
+
title,
|
|
1966
|
+
subtitle,
|
|
1967
|
+
icon,
|
|
1968
|
+
children,
|
|
1969
|
+
footer,
|
|
1970
|
+
maxWidth = "lg",
|
|
1971
|
+
systemColor,
|
|
1972
|
+
closeOnBackdrop = true
|
|
1973
|
+
}) {
|
|
1974
|
+
if (!isOpen) return null;
|
|
1975
|
+
const handleBackdropClick = () => {
|
|
1976
|
+
if (closeOnBackdrop) {
|
|
1977
|
+
onClose();
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
1981
|
+
/* @__PURE__ */ jsx(
|
|
1982
|
+
"div",
|
|
1983
|
+
{
|
|
1984
|
+
className: "absolute inset-0 bg-black/80 backdrop-blur-sm",
|
|
1985
|
+
onClick: handleBackdropClick
|
|
1986
|
+
}
|
|
1987
|
+
),
|
|
1988
|
+
/* @__PURE__ */ jsxs(
|
|
1989
|
+
"div",
|
|
1990
|
+
{
|
|
1991
|
+
className: `relative bg-gray-900 border border-retro-primary/30 rounded-xl shadow-2xl w-full ${MAX_WIDTH_CLASSES[maxWidth]} mx-4 overflow-hidden`,
|
|
1992
|
+
style: systemColor ? { borderColor: `${systemColor}30` } : void 0,
|
|
1993
|
+
children: [
|
|
1994
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
|
|
1995
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1996
|
+
/* @__PURE__ */ jsx("div", { style: systemColor ? { color: systemColor } : void 0, children: icon }),
|
|
1997
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1998
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: title }),
|
|
1999
|
+
subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: subtitle })
|
|
2000
|
+
] })
|
|
2001
|
+
] }),
|
|
2002
|
+
/* @__PURE__ */ jsx(
|
|
2003
|
+
"button",
|
|
2004
|
+
{
|
|
2005
|
+
onClick: onClose,
|
|
2006
|
+
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
2007
|
+
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
2008
|
+
}
|
|
2009
|
+
)
|
|
2010
|
+
] }),
|
|
2011
|
+
children,
|
|
2012
|
+
footer && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: footer })
|
|
2013
|
+
]
|
|
2014
|
+
}
|
|
2015
|
+
)
|
|
2016
|
+
] });
|
|
2017
|
+
}
|
|
1935
2018
|
var ShortcutsModal = memo(function ShortcutsModal2({
|
|
1936
2019
|
isOpen,
|
|
1937
2020
|
onClose,
|
|
@@ -1960,90 +2043,55 @@ var ShortcutsModal = memo(function ShortcutsModal2({
|
|
|
1960
2043
|
]
|
|
1961
2044
|
}
|
|
1962
2045
|
], [t]);
|
|
1963
|
-
if (!isOpen) return null;
|
|
1964
2046
|
return /* @__PURE__ */ jsx(
|
|
1965
|
-
|
|
2047
|
+
ModalShell,
|
|
1966
2048
|
{
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
className: "p-1 rounded hover:bg-white/10 transition-colors",
|
|
1991
|
-
children: /* @__PURE__ */ jsx(X, { size: 18, className: "text-white/60 hover:text-white" })
|
|
1992
|
-
}
|
|
1993
|
-
)
|
|
1994
|
-
]
|
|
1995
|
-
}
|
|
1996
|
-
),
|
|
1997
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
|
|
1998
|
-
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2049
|
+
isOpen,
|
|
2050
|
+
onClose,
|
|
2051
|
+
title: t.modals.shortcuts.playerShortcuts,
|
|
2052
|
+
icon: /* @__PURE__ */ jsx(Keyboard, { size: 20, style: { color: systemColor } }),
|
|
2053
|
+
maxWidth: "sm",
|
|
2054
|
+
systemColor,
|
|
2055
|
+
footer: /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 w-full text-center", children: t.modals.shortcuts.pressEsc }),
|
|
2056
|
+
children: /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3", children: [
|
|
2057
|
+
shortcuts.map(({ section, items }) => /* @__PURE__ */ jsxs("div", { children: [
|
|
2058
|
+
/* @__PURE__ */ jsx(
|
|
2059
|
+
"h3",
|
|
2060
|
+
{
|
|
2061
|
+
className: "text-[10px] font-bold uppercase tracking-wider mb-1.5 opacity-60",
|
|
2062
|
+
style: { color: systemColor },
|
|
2063
|
+
children: section
|
|
2064
|
+
}
|
|
2065
|
+
),
|
|
2066
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: items.map(({ key, description }) => /* @__PURE__ */ jsxs(
|
|
2067
|
+
"div",
|
|
2068
|
+
{
|
|
2069
|
+
className: "flex items-center justify-between text-sm",
|
|
2070
|
+
children: [
|
|
2071
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/70", children: description }),
|
|
1999
2072
|
/* @__PURE__ */ jsx(
|
|
2000
|
-
"
|
|
2073
|
+
"kbd",
|
|
2001
2074
|
{
|
|
2002
|
-
className: "
|
|
2003
|
-
style: {
|
|
2004
|
-
|
|
2075
|
+
className: "px-2 py-0.5 rounded text-xs font-mono font-bold",
|
|
2076
|
+
style: {
|
|
2077
|
+
backgroundColor: `${systemColor}20`,
|
|
2078
|
+
color: systemColor,
|
|
2079
|
+
border: `1px solid ${systemColor}40`
|
|
2080
|
+
},
|
|
2081
|
+
children: key
|
|
2005
2082
|
}
|
|
2006
|
-
)
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
backgroundColor: `${systemColor}20`,
|
|
2019
|
-
color: systemColor,
|
|
2020
|
-
border: `1px solid ${systemColor}40`
|
|
2021
|
-
},
|
|
2022
|
-
children: key
|
|
2023
|
-
}
|
|
2024
|
-
)
|
|
2025
|
-
]
|
|
2026
|
-
},
|
|
2027
|
-
key
|
|
2028
|
-
)) })
|
|
2029
|
-
] }, section)),
|
|
2030
|
-
/* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2031
|
-
"Game controls can be configured in ",
|
|
2032
|
-
/* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2033
|
-
" settings."
|
|
2034
|
-
] })
|
|
2035
|
-
] }),
|
|
2036
|
-
/* @__PURE__ */ jsx(
|
|
2037
|
-
"div",
|
|
2038
|
-
{
|
|
2039
|
-
className: "px-4 py-2 text-center text-xs text-white/40 border-t",
|
|
2040
|
-
style: { borderColor: `${systemColor}20` },
|
|
2041
|
-
children: t.modals.shortcuts.pressEsc
|
|
2042
|
-
}
|
|
2043
|
-
)
|
|
2044
|
-
]
|
|
2045
|
-
}
|
|
2046
|
-
)
|
|
2083
|
+
)
|
|
2084
|
+
]
|
|
2085
|
+
},
|
|
2086
|
+
key
|
|
2087
|
+
)) })
|
|
2088
|
+
] }, section)),
|
|
2089
|
+
/* @__PURE__ */ jsxs("div", { className: "pt-2 border-t border-white/10 text-xs text-white/40", children: [
|
|
2090
|
+
"Game controls can be configured in ",
|
|
2091
|
+
/* @__PURE__ */ jsx("strong", { className: "text-white/60", children: t.controls.keys }),
|
|
2092
|
+
" settings."
|
|
2093
|
+
] })
|
|
2094
|
+
] })
|
|
2047
2095
|
}
|
|
2048
2096
|
);
|
|
2049
2097
|
});
|
|
@@ -2640,6 +2688,39 @@ function useTouchHandlers({
|
|
|
2640
2688
|
cleanup
|
|
2641
2689
|
};
|
|
2642
2690
|
}
|
|
2691
|
+
function useTouchEvents(ref, handlers, options = {}) {
|
|
2692
|
+
const { cleanup, passive = false } = options;
|
|
2693
|
+
const handlersRef = useRef(handlers);
|
|
2694
|
+
handlersRef.current = handlers;
|
|
2695
|
+
const handleTouchStart = useCallback((e) => {
|
|
2696
|
+
handlersRef.current.onTouchStart?.(e);
|
|
2697
|
+
}, []);
|
|
2698
|
+
const handleTouchMove = useCallback((e) => {
|
|
2699
|
+
handlersRef.current.onTouchMove?.(e);
|
|
2700
|
+
}, []);
|
|
2701
|
+
const handleTouchEnd = useCallback((e) => {
|
|
2702
|
+
handlersRef.current.onTouchEnd?.(e);
|
|
2703
|
+
}, []);
|
|
2704
|
+
const handleTouchCancel = useCallback((e) => {
|
|
2705
|
+
handlersRef.current.onTouchCancel?.(e);
|
|
2706
|
+
}, []);
|
|
2707
|
+
useEffect(() => {
|
|
2708
|
+
const element = ref.current;
|
|
2709
|
+
if (!element) return;
|
|
2710
|
+
const listenerOptions = { passive };
|
|
2711
|
+
element.addEventListener("touchstart", handleTouchStart, listenerOptions);
|
|
2712
|
+
element.addEventListener("touchmove", handleTouchMove, listenerOptions);
|
|
2713
|
+
element.addEventListener("touchend", handleTouchEnd, listenerOptions);
|
|
2714
|
+
element.addEventListener("touchcancel", handleTouchCancel, listenerOptions);
|
|
2715
|
+
return () => {
|
|
2716
|
+
element.removeEventListener("touchstart", handleTouchStart);
|
|
2717
|
+
element.removeEventListener("touchmove", handleTouchMove);
|
|
2718
|
+
element.removeEventListener("touchend", handleTouchEnd);
|
|
2719
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
2720
|
+
cleanup?.();
|
|
2721
|
+
};
|
|
2722
|
+
}, [ref, passive, cleanup, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
2723
|
+
}
|
|
2643
2724
|
|
|
2644
2725
|
// src/components/VirtualController/utils/buttonStyles.ts
|
|
2645
2726
|
var DEFAULT_FACE = {
|
|
@@ -2797,21 +2878,12 @@ var VirtualButton = React2.memo(function VirtualButton2({
|
|
|
2797
2878
|
onRelease,
|
|
2798
2879
|
onPositionChange
|
|
2799
2880
|
});
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
button.addEventListener("touchcancel", handleTouchCancel, { passive: false });
|
|
2807
|
-
return () => {
|
|
2808
|
-
button.removeEventListener("touchstart", handleTouchStart);
|
|
2809
|
-
button.removeEventListener("touchmove", handleTouchMove);
|
|
2810
|
-
button.removeEventListener("touchend", handleTouchEnd);
|
|
2811
|
-
button.removeEventListener("touchcancel", handleTouchCancel);
|
|
2812
|
-
cleanup();
|
|
2813
|
-
};
|
|
2814
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, cleanup]);
|
|
2881
|
+
useTouchEvents(buttonRef, {
|
|
2882
|
+
onTouchStart: handleTouchStart,
|
|
2883
|
+
onTouchMove: handleTouchMove,
|
|
2884
|
+
onTouchEnd: handleTouchEnd,
|
|
2885
|
+
onTouchCancel: handleTouchCancel
|
|
2886
|
+
}, { cleanup });
|
|
2815
2887
|
const leftPercent = displayX / 100 * containerWidth - config.size / 2;
|
|
2816
2888
|
const topPercent = displayY / 100 * containerHeight - config.size / 2;
|
|
2817
2889
|
const transform = `translate3d(${leftPercent.toFixed(1)}px, ${topPercent.toFixed(1)}px, 0)`;
|
|
@@ -3487,7 +3559,7 @@ function dispatchKeyboardEvent(type, code) {
|
|
|
3487
3559
|
canvas.dispatchEvent(event);
|
|
3488
3560
|
return true;
|
|
3489
3561
|
}
|
|
3490
|
-
var CENTER_TOUCH_RADIUS = 0.
|
|
3562
|
+
var CENTER_TOUCH_RADIUS = 0.35;
|
|
3491
3563
|
var Dpad = React2.memo(function Dpad2({
|
|
3492
3564
|
size = 180,
|
|
3493
3565
|
x,
|
|
@@ -3661,21 +3733,12 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3661
3733
|
}
|
|
3662
3734
|
}
|
|
3663
3735
|
}, [getKeyCode, updateVisuals, drag]);
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
dpad.addEventListener("touchcancel", handleTouchEnd, { passive: false });
|
|
3671
|
-
return () => {
|
|
3672
|
-
dpad.removeEventListener("touchstart", handleTouchStart);
|
|
3673
|
-
dpad.removeEventListener("touchmove", handleTouchMove);
|
|
3674
|
-
dpad.removeEventListener("touchend", handleTouchEnd);
|
|
3675
|
-
dpad.removeEventListener("touchcancel", handleTouchEnd);
|
|
3676
|
-
drag.clearDragTimer();
|
|
3677
|
-
};
|
|
3678
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd, drag]);
|
|
3736
|
+
useTouchEvents(dpadRef, {
|
|
3737
|
+
onTouchStart: handleTouchStart,
|
|
3738
|
+
onTouchMove: handleTouchMove,
|
|
3739
|
+
onTouchEnd: handleTouchEnd,
|
|
3740
|
+
onTouchCancel: handleTouchEnd
|
|
3741
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3679
3742
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3680
3743
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3681
3744
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3875,6 +3938,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3875
3938
|
}
|
|
3876
3939
|
);
|
|
3877
3940
|
}
|
|
3941
|
+
function LockButton({
|
|
3942
|
+
isLocked,
|
|
3943
|
+
onToggle,
|
|
3944
|
+
systemColor = "#00FF41"
|
|
3945
|
+
}) {
|
|
3946
|
+
const Icon = isLocked ? Lock : Unlock;
|
|
3947
|
+
return /* @__PURE__ */ jsx(
|
|
3948
|
+
"button",
|
|
3949
|
+
{
|
|
3950
|
+
onClick: onToggle,
|
|
3951
|
+
className: "fixed top-4 left-1/2 -translate-x-1/2 z-40 pointer-events-auto p-2 rounded-full backdrop-blur-sm transition-all active:scale-95",
|
|
3952
|
+
style: {
|
|
3953
|
+
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3954
|
+
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
3955
|
+
},
|
|
3956
|
+
"aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
|
|
3957
|
+
children: /* @__PURE__ */ jsx(
|
|
3958
|
+
Icon,
|
|
3959
|
+
{
|
|
3960
|
+
size: 18,
|
|
3961
|
+
style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
|
|
3962
|
+
}
|
|
3963
|
+
)
|
|
3964
|
+
}
|
|
3965
|
+
);
|
|
3966
|
+
}
|
|
3967
|
+
var LOCK_KEY = "koin-controls-locked";
|
|
3878
3968
|
function VirtualController({
|
|
3879
3969
|
system,
|
|
3880
3970
|
isRunning,
|
|
@@ -3886,7 +3976,21 @@ function VirtualController({
|
|
|
3886
3976
|
const [pressedButtons, setPressedButtons] = useState(/* @__PURE__ */ new Set());
|
|
3887
3977
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
3888
3978
|
const [isFullscreenState, setIsFullscreenState] = useState(false);
|
|
3979
|
+
const [isLocked, setIsLocked] = useState(true);
|
|
3889
3980
|
const { getPosition, savePosition } = useButtonPositions();
|
|
3981
|
+
useEffect(() => {
|
|
3982
|
+
const stored = localStorage.getItem(LOCK_KEY);
|
|
3983
|
+
if (stored !== null) {
|
|
3984
|
+
setIsLocked(stored === "true");
|
|
3985
|
+
}
|
|
3986
|
+
}, []);
|
|
3987
|
+
const toggleLock = useCallback(() => {
|
|
3988
|
+
setIsLocked((prev) => {
|
|
3989
|
+
const newValue = !prev;
|
|
3990
|
+
localStorage.setItem(LOCK_KEY, String(newValue));
|
|
3991
|
+
return newValue;
|
|
3992
|
+
});
|
|
3993
|
+
}, []);
|
|
3890
3994
|
const layout = getLayoutForSystem(system);
|
|
3891
3995
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3892
3996
|
if (isPortrait) {
|
|
@@ -4041,6 +4145,14 @@ function VirtualController({
|
|
|
4041
4145
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
4042
4146
|
style: { touchAction: "none" },
|
|
4043
4147
|
children: [
|
|
4148
|
+
/* @__PURE__ */ jsx(
|
|
4149
|
+
LockButton,
|
|
4150
|
+
{
|
|
4151
|
+
isLocked,
|
|
4152
|
+
onToggle: toggleLock,
|
|
4153
|
+
systemColor
|
|
4154
|
+
}
|
|
4155
|
+
),
|
|
4044
4156
|
/* @__PURE__ */ jsx(
|
|
4045
4157
|
Dpad_default,
|
|
4046
4158
|
{
|
|
@@ -4053,7 +4165,7 @@ function VirtualController({
|
|
|
4053
4165
|
systemColor,
|
|
4054
4166
|
isLandscape,
|
|
4055
4167
|
customPosition: getPosition("up", isLandscape),
|
|
4056
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4168
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
4057
4169
|
}
|
|
4058
4170
|
),
|
|
4059
4171
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
|
|
@@ -4067,7 +4179,7 @@ function VirtualController({
|
|
|
4067
4179
|
containerWidth: width,
|
|
4068
4180
|
containerHeight: height,
|
|
4069
4181
|
customPosition,
|
|
4070
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4182
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4071
4183
|
isLandscape,
|
|
4072
4184
|
console: layout.console
|
|
4073
4185
|
},
|
|
@@ -4387,6 +4499,45 @@ var GameCanvas = memo(function GameCanvas2({
|
|
|
4387
4499
|
] });
|
|
4388
4500
|
});
|
|
4389
4501
|
var GameCanvas_default = GameCanvas;
|
|
4502
|
+
function useInputCapture({
|
|
4503
|
+
isOpen,
|
|
4504
|
+
onClose
|
|
4505
|
+
}) {
|
|
4506
|
+
const [listeningFor, setListeningFor] = useState(null);
|
|
4507
|
+
const startListening = useCallback((target) => {
|
|
4508
|
+
setListeningFor(target);
|
|
4509
|
+
}, []);
|
|
4510
|
+
const stopListening = useCallback(() => {
|
|
4511
|
+
setListeningFor(null);
|
|
4512
|
+
}, []);
|
|
4513
|
+
useEffect(() => {
|
|
4514
|
+
if (!isOpen) {
|
|
4515
|
+
setListeningFor(null);
|
|
4516
|
+
}
|
|
4517
|
+
}, [isOpen]);
|
|
4518
|
+
useEffect(() => {
|
|
4519
|
+
if (!isOpen) return;
|
|
4520
|
+
const handleKeyDown = (e) => {
|
|
4521
|
+
if (e.code === "Escape") {
|
|
4522
|
+
if (listeningFor !== null) {
|
|
4523
|
+
e.preventDefault();
|
|
4524
|
+
e.stopPropagation();
|
|
4525
|
+
setListeningFor(null);
|
|
4526
|
+
} else {
|
|
4527
|
+
onClose();
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4531
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4532
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4533
|
+
}, [isOpen, listeningFor, onClose]);
|
|
4534
|
+
return {
|
|
4535
|
+
listeningFor,
|
|
4536
|
+
startListening,
|
|
4537
|
+
stopListening,
|
|
4538
|
+
isListening: listeningFor !== null
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4390
4541
|
function getFilteredGroups(activeButtons) {
|
|
4391
4542
|
return BUTTON_GROUPS.map((group) => ({
|
|
4392
4543
|
...group,
|
|
@@ -4402,7 +4553,10 @@ function ControlMapper({
|
|
|
4402
4553
|
}) {
|
|
4403
4554
|
const t = useKoinTranslation();
|
|
4404
4555
|
const [localControls, setLocalControls] = useState(controls);
|
|
4405
|
-
const
|
|
4556
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4557
|
+
isOpen,
|
|
4558
|
+
onClose
|
|
4559
|
+
});
|
|
4406
4560
|
const activeButtons = useMemo(() => {
|
|
4407
4561
|
return getConsoleButtons(system || "SNES");
|
|
4408
4562
|
}, [system]);
|
|
@@ -4418,27 +4572,20 @@ function ControlMapper({
|
|
|
4418
4572
|
}
|
|
4419
4573
|
}, [isOpen, controls]);
|
|
4420
4574
|
useEffect(() => {
|
|
4421
|
-
if (!isOpen)
|
|
4422
|
-
setListeningFor(null);
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4575
|
+
if (!isOpen || !listeningFor) return;
|
|
4425
4576
|
const handleKeyDown = (e) => {
|
|
4426
|
-
if (
|
|
4577
|
+
if (e.code === "Escape") return;
|
|
4427
4578
|
e.preventDefault();
|
|
4428
4579
|
e.stopPropagation();
|
|
4429
|
-
if (e.code === "Escape") {
|
|
4430
|
-
setListeningFor(null);
|
|
4431
|
-
return;
|
|
4432
|
-
}
|
|
4433
4580
|
setLocalControls((prev) => ({
|
|
4434
4581
|
...prev,
|
|
4435
4582
|
[listeningFor]: e.code
|
|
4436
4583
|
}));
|
|
4437
|
-
|
|
4584
|
+
stopListening();
|
|
4438
4585
|
};
|
|
4439
4586
|
window.addEventListener("keydown", handleKeyDown);
|
|
4440
4587
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4441
|
-
}, [isOpen, listeningFor]);
|
|
4588
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4442
4589
|
const handleReset = () => {
|
|
4443
4590
|
setLocalControls(defaultControls);
|
|
4444
4591
|
};
|
|
@@ -4446,52 +4593,58 @@ function ControlMapper({
|
|
|
4446
4593
|
onSave(localControls);
|
|
4447
4594
|
onClose();
|
|
4448
4595
|
};
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4596
|
+
return /* @__PURE__ */ jsx(
|
|
4597
|
+
ModalShell,
|
|
4598
|
+
{
|
|
4599
|
+
isOpen,
|
|
4600
|
+
onClose,
|
|
4601
|
+
title: t.modals.controls.title,
|
|
4602
|
+
subtitle: t.modals.controls.description,
|
|
4603
|
+
icon: /* @__PURE__ */ jsx(Gamepad2, { className: "text-retro-primary", size: 24 }),
|
|
4604
|
+
closeOnBackdrop: !isListening,
|
|
4605
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4606
|
+
/* @__PURE__ */ jsxs(
|
|
4607
|
+
"button",
|
|
4608
|
+
{
|
|
4609
|
+
onClick: handleReset,
|
|
4610
|
+
className: "flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-white hover:bg-white/10 transition-colors",
|
|
4611
|
+
children: [
|
|
4612
|
+
/* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
|
|
4613
|
+
t.modals.controls.reset
|
|
4614
|
+
]
|
|
4615
|
+
}
|
|
4616
|
+
),
|
|
4617
|
+
/* @__PURE__ */ jsxs(
|
|
4468
4618
|
"button",
|
|
4469
4619
|
{
|
|
4470
|
-
onClick:
|
|
4471
|
-
className: "
|
|
4472
|
-
children:
|
|
4620
|
+
onClick: handleSave,
|
|
4621
|
+
className: "flex items-center gap-2 px-6 py-2 rounded-lg bg-retro-primary text-black font-bold text-sm hover:bg-retro-primary/90 transition-colors",
|
|
4622
|
+
children: [
|
|
4623
|
+
/* @__PURE__ */ jsx(Check, { size: 16 }),
|
|
4624
|
+
t.modals.controls.save
|
|
4625
|
+
]
|
|
4473
4626
|
}
|
|
4474
4627
|
)
|
|
4475
4628
|
] }),
|
|
4476
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4629
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4477
4630
|
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4478
4631
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4479
4632
|
"button",
|
|
4480
4633
|
{
|
|
4481
|
-
onClick: () =>
|
|
4634
|
+
onClick: () => startListening(btn),
|
|
4482
4635
|
className: `
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4636
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4637
|
+
${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4638
|
+
`,
|
|
4486
4639
|
children: [
|
|
4487
4640
|
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4488
4641
|
/* @__PURE__ */ jsx(
|
|
4489
4642
|
"span",
|
|
4490
4643
|
{
|
|
4491
4644
|
className: `
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4645
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4646
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4647
|
+
`,
|
|
4495
4648
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4496
4649
|
}
|
|
4497
4650
|
)
|
|
@@ -4499,199 +4652,10 @@ function ControlMapper({
|
|
|
4499
4652
|
},
|
|
4500
4653
|
btn
|
|
4501
4654
|
)) })
|
|
4502
|
-
] }, group.label)) })
|
|
4503
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4504
|
-
/* @__PURE__ */ jsxs(
|
|
4505
|
-
"button",
|
|
4506
|
-
{
|
|
4507
|
-
onClick: handleReset,
|
|
4508
|
-
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",
|
|
4509
|
-
children: [
|
|
4510
|
-
/* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
|
|
4511
|
-
t.modals.controls.reset
|
|
4512
|
-
]
|
|
4513
|
-
}
|
|
4514
|
-
),
|
|
4515
|
-
/* @__PURE__ */ jsxs(
|
|
4516
|
-
"button",
|
|
4517
|
-
{
|
|
4518
|
-
onClick: handleSave,
|
|
4519
|
-
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",
|
|
4520
|
-
children: [
|
|
4521
|
-
/* @__PURE__ */ jsx(Check, { size: 16 }),
|
|
4522
|
-
t.modals.controls.save
|
|
4523
|
-
]
|
|
4524
|
-
}
|
|
4525
|
-
)
|
|
4526
|
-
] })
|
|
4527
|
-
] })
|
|
4528
|
-
] });
|
|
4529
|
-
}
|
|
4530
|
-
function getDisplayName(id) {
|
|
4531
|
-
let name = id;
|
|
4532
|
-
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
4533
|
-
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
4534
|
-
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
4535
|
-
if (/xbox/i.test(name)) {
|
|
4536
|
-
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
4537
|
-
if (/one/i.test(name)) return "Xbox One Controller";
|
|
4538
|
-
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
4539
|
-
return "Xbox Controller";
|
|
4540
|
-
}
|
|
4541
|
-
if (/dualsense/i.test(name)) return "DualSense";
|
|
4542
|
-
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
4543
|
-
if (/dualshock/i.test(name)) return "DualShock";
|
|
4544
|
-
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
4545
|
-
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
4546
|
-
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
4547
|
-
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
4548
|
-
return name.trim() || "Gamepad";
|
|
4549
|
-
}
|
|
4550
|
-
function detectControllerBrand(id) {
|
|
4551
|
-
const lowerId = id.toLowerCase();
|
|
4552
|
-
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
4553
|
-
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
4554
|
-
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
4555
|
-
return "generic";
|
|
4556
|
-
}
|
|
4557
|
-
function toGamepadInfo(gamepad) {
|
|
4558
|
-
return {
|
|
4559
|
-
index: gamepad.index,
|
|
4560
|
-
id: gamepad.id,
|
|
4561
|
-
name: getDisplayName(gamepad.id),
|
|
4562
|
-
connected: gamepad.connected,
|
|
4563
|
-
buttons: gamepad.buttons.length,
|
|
4564
|
-
axes: gamepad.axes.length,
|
|
4565
|
-
mapping: gamepad.mapping
|
|
4566
|
-
};
|
|
4567
|
-
}
|
|
4568
|
-
function useGamepad(options) {
|
|
4569
|
-
const { onConnect, onDisconnect } = options || {};
|
|
4570
|
-
const [gamepads, setGamepads] = useState([]);
|
|
4571
|
-
const rafRef = useRef(null);
|
|
4572
|
-
const lastStateRef = useRef("");
|
|
4573
|
-
const prevCountRef = useRef(0);
|
|
4574
|
-
const onConnectRef = useRef(onConnect);
|
|
4575
|
-
const onDisconnectRef = useRef(onDisconnect);
|
|
4576
|
-
useEffect(() => {
|
|
4577
|
-
onConnectRef.current = onConnect;
|
|
4578
|
-
onDisconnectRef.current = onDisconnect;
|
|
4579
|
-
}, [onConnect, onDisconnect]);
|
|
4580
|
-
const getGamepads = useCallback(() => {
|
|
4581
|
-
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
4582
|
-
return [];
|
|
4655
|
+
] }, group.label)) })
|
|
4583
4656
|
}
|
|
4584
|
-
|
|
4585
|
-
const connected = [];
|
|
4586
|
-
for (let i = 0; i < rawGamepads.length; i++) {
|
|
4587
|
-
const gp = rawGamepads[i];
|
|
4588
|
-
if (gp && gp.connected) {
|
|
4589
|
-
connected.push(toGamepadInfo(gp));
|
|
4590
|
-
}
|
|
4591
|
-
}
|
|
4592
|
-
return connected;
|
|
4593
|
-
}, []);
|
|
4594
|
-
const getRawGamepad = useCallback((index) => {
|
|
4595
|
-
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
4596
|
-
return rawGamepads[index] ?? null;
|
|
4597
|
-
}, []);
|
|
4598
|
-
const refresh = useCallback(() => {
|
|
4599
|
-
setGamepads(getGamepads());
|
|
4600
|
-
}, [getGamepads]);
|
|
4601
|
-
useEffect(() => {
|
|
4602
|
-
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
4603
|
-
return;
|
|
4604
|
-
}
|
|
4605
|
-
if (typeof navigator.getGamepads !== "function") {
|
|
4606
|
-
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
4607
|
-
return;
|
|
4608
|
-
}
|
|
4609
|
-
let isActive = true;
|
|
4610
|
-
const poll = () => {
|
|
4611
|
-
if (!isActive) return;
|
|
4612
|
-
const current = getGamepads();
|
|
4613
|
-
let hasChanged = current.length !== prevCountRef.current;
|
|
4614
|
-
if (!hasChanged) {
|
|
4615
|
-
for (let i = 0; i < current.length; i++) {
|
|
4616
|
-
const saved = gamepads[i];
|
|
4617
|
-
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
4618
|
-
hasChanged = true;
|
|
4619
|
-
break;
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
if (hasChanged) {
|
|
4624
|
-
const prevCount = prevCountRef.current;
|
|
4625
|
-
const currentCount = current.length;
|
|
4626
|
-
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
4627
|
-
const newGamepad = current[current.length - 1];
|
|
4628
|
-
onConnectRef.current(newGamepad);
|
|
4629
|
-
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
4630
|
-
onDisconnectRef.current();
|
|
4631
|
-
}
|
|
4632
|
-
prevCountRef.current = currentCount;
|
|
4633
|
-
setGamepads(current);
|
|
4634
|
-
}
|
|
4635
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4636
|
-
};
|
|
4637
|
-
const handleConnect = (e) => {
|
|
4638
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
4639
|
-
const current = getGamepads();
|
|
4640
|
-
const prevCount = prevCountRef.current;
|
|
4641
|
-
prevCountRef.current = current.length;
|
|
4642
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4643
|
-
setGamepads(current);
|
|
4644
|
-
if (onConnectRef.current && current.length > prevCount) {
|
|
4645
|
-
const newGamepad = current[current.length - 1];
|
|
4646
|
-
onConnectRef.current(newGamepad);
|
|
4647
|
-
}
|
|
4648
|
-
};
|
|
4649
|
-
const handleDisconnect = (e) => {
|
|
4650
|
-
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
4651
|
-
const current = getGamepads();
|
|
4652
|
-
const prevCount = prevCountRef.current;
|
|
4653
|
-
prevCountRef.current = current.length;
|
|
4654
|
-
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4655
|
-
setGamepads(current);
|
|
4656
|
-
if (onDisconnectRef.current && current.length < prevCount) {
|
|
4657
|
-
onDisconnectRef.current();
|
|
4658
|
-
}
|
|
4659
|
-
};
|
|
4660
|
-
window.addEventListener("gamepadconnected", handleConnect);
|
|
4661
|
-
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
4662
|
-
rafRef.current = requestAnimationFrame(poll);
|
|
4663
|
-
const initial = getGamepads();
|
|
4664
|
-
if (initial.length > 0) {
|
|
4665
|
-
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
4666
|
-
prevCountRef.current = initial.length;
|
|
4667
|
-
setGamepads(initial);
|
|
4668
|
-
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
4669
|
-
} else {
|
|
4670
|
-
prevCountRef.current = 0;
|
|
4671
|
-
}
|
|
4672
|
-
return () => {
|
|
4673
|
-
isActive = false;
|
|
4674
|
-
if (rafRef.current) {
|
|
4675
|
-
cancelAnimationFrame(rafRef.current);
|
|
4676
|
-
}
|
|
4677
|
-
window.removeEventListener("gamepadconnected", handleConnect);
|
|
4678
|
-
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
4679
|
-
};
|
|
4680
|
-
}, [getGamepads]);
|
|
4681
|
-
return {
|
|
4682
|
-
gamepads,
|
|
4683
|
-
isAnyConnected: gamepads.length > 0,
|
|
4684
|
-
connectedCount: gamepads.length,
|
|
4685
|
-
getRawGamepad,
|
|
4686
|
-
refresh
|
|
4687
|
-
};
|
|
4657
|
+
);
|
|
4688
4658
|
}
|
|
4689
|
-
var STANDARD_AXIS_MAP = {
|
|
4690
|
-
leftStickX: 0,
|
|
4691
|
-
leftStickY: 1,
|
|
4692
|
-
rightStickX: 2,
|
|
4693
|
-
rightStickY: 3
|
|
4694
|
-
};
|
|
4695
4659
|
function GamepadMapper({
|
|
4696
4660
|
isOpen,
|
|
4697
4661
|
gamepads,
|
|
@@ -4702,7 +4666,10 @@ function GamepadMapper({
|
|
|
4702
4666
|
const t = useKoinTranslation();
|
|
4703
4667
|
const [selectedPlayer, setSelectedPlayer] = useState(1);
|
|
4704
4668
|
const [bindings, setBindings] = useState({});
|
|
4705
|
-
const
|
|
4669
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4670
|
+
isOpen,
|
|
4671
|
+
onClose
|
|
4672
|
+
});
|
|
4706
4673
|
const rafRef = useRef(null);
|
|
4707
4674
|
useEffect(() => {
|
|
4708
4675
|
if (isOpen) {
|
|
@@ -4737,7 +4704,7 @@ function GamepadMapper({
|
|
|
4737
4704
|
[listeningFor]: i
|
|
4738
4705
|
}
|
|
4739
4706
|
}));
|
|
4740
|
-
|
|
4707
|
+
stopListening();
|
|
4741
4708
|
return;
|
|
4742
4709
|
}
|
|
4743
4710
|
}
|
|
@@ -4750,21 +4717,7 @@ function GamepadMapper({
|
|
|
4750
4717
|
cancelAnimationFrame(rafRef.current);
|
|
4751
4718
|
}
|
|
4752
4719
|
};
|
|
4753
|
-
}, [isOpen, listeningFor, selectedPlayer]);
|
|
4754
|
-
useEffect(() => {
|
|
4755
|
-
if (!isOpen) return;
|
|
4756
|
-
const handleKeyDown = (e) => {
|
|
4757
|
-
if (e.code === "Escape") {
|
|
4758
|
-
if (listeningFor) {
|
|
4759
|
-
setListeningFor(null);
|
|
4760
|
-
} else {
|
|
4761
|
-
onClose();
|
|
4762
|
-
}
|
|
4763
|
-
}
|
|
4764
|
-
};
|
|
4765
|
-
window.addEventListener("keydown", handleKeyDown);
|
|
4766
|
-
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4767
|
-
}, [isOpen, listeningFor, onClose]);
|
|
4720
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4768
4721
|
const handleReset = () => {
|
|
4769
4722
|
setBindings((prev) => ({
|
|
4770
4723
|
...prev,
|
|
@@ -4779,127 +4732,19 @@ function GamepadMapper({
|
|
|
4779
4732
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4780
4733
|
onClose();
|
|
4781
4734
|
};
|
|
4782
|
-
if (!isOpen) return null;
|
|
4783
4735
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4784
4736
|
const currentGamepad = gamepads.find((g) => g.index === selectedPlayer - 1);
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
}
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
/* @__PURE__ */ jsxs(
|
|
4796
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4797
|
-
/* @__PURE__ */ jsx(Joystick, { className: "text-retro-primary", size: 24, style: { color: systemColor } }),
|
|
4798
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
4799
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.gamepad.title }),
|
|
4800
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none })
|
|
4801
|
-
] })
|
|
4802
|
-
] }),
|
|
4803
|
-
/* @__PURE__ */ jsx(
|
|
4804
|
-
"button",
|
|
4805
|
-
{
|
|
4806
|
-
onClick: onClose,
|
|
4807
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4808
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
4809
|
-
}
|
|
4810
|
-
)
|
|
4811
|
-
] }),
|
|
4812
|
-
gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4813
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4814
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4815
|
-
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
|
|
4816
|
-
"button",
|
|
4817
|
-
{
|
|
4818
|
-
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4819
|
-
className: `
|
|
4820
|
-
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4821
|
-
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4822
|
-
`,
|
|
4823
|
-
style: selectedPlayer === gp.index + 1 ? {
|
|
4824
|
-
backgroundColor: `${systemColor}20`,
|
|
4825
|
-
color: systemColor
|
|
4826
|
-
} : {},
|
|
4827
|
-
children: [
|
|
4828
|
-
/* @__PURE__ */ jsx(User, { size: 14 }),
|
|
4829
|
-
"P",
|
|
4830
|
-
gp.index + 1
|
|
4831
|
-
]
|
|
4832
|
-
},
|
|
4833
|
-
gp.index
|
|
4834
|
-
)) })
|
|
4835
|
-
] }),
|
|
4836
|
-
currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4837
|
-
] }),
|
|
4838
|
-
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4839
|
-
currentGamepad.name,
|
|
4840
|
-
" \u2022 Player 1"
|
|
4841
|
-
] }) }),
|
|
4842
|
-
gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4843
|
-
/* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4844
|
-
/* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4845
|
-
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4846
|
-
] }),
|
|
4847
|
-
/* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4848
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4849
|
-
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4850
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4851
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
4852
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4853
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4854
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4855
|
-
] })
|
|
4856
|
-
] })
|
|
4857
|
-
] }),
|
|
4858
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4859
|
-
listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4860
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4861
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4862
|
-
] }),
|
|
4863
|
-
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4864
|
-
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4865
|
-
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4866
|
-
"button",
|
|
4867
|
-
{
|
|
4868
|
-
onClick: () => setListeningFor(btn),
|
|
4869
|
-
disabled: !!listeningFor && listeningFor !== btn,
|
|
4870
|
-
className: `
|
|
4871
|
-
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4872
|
-
disabled:opacity-50
|
|
4873
|
-
${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"}
|
|
4874
|
-
`,
|
|
4875
|
-
style: listeningFor === btn ? {
|
|
4876
|
-
borderColor: systemColor,
|
|
4877
|
-
backgroundColor: `${systemColor}20`,
|
|
4878
|
-
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4879
|
-
} : {},
|
|
4880
|
-
children: [
|
|
4881
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4882
|
-
/* @__PURE__ */ jsx(
|
|
4883
|
-
"span",
|
|
4884
|
-
{
|
|
4885
|
-
className: `
|
|
4886
|
-
px-2 py-1 rounded text-xs font-mono
|
|
4887
|
-
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4888
|
-
`,
|
|
4889
|
-
style: listeningFor === btn ? {
|
|
4890
|
-
backgroundColor: `${systemColor}30`,
|
|
4891
|
-
color: systemColor
|
|
4892
|
-
} : {},
|
|
4893
|
-
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4894
|
-
}
|
|
4895
|
-
)
|
|
4896
|
-
]
|
|
4897
|
-
},
|
|
4898
|
-
btn
|
|
4899
|
-
)) })
|
|
4900
|
-
] }, group.label))
|
|
4901
|
-
] }),
|
|
4902
|
-
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 bg-black/30 border-t border-white/10", children: [
|
|
4737
|
+
return /* @__PURE__ */ jsxs(
|
|
4738
|
+
ModalShell,
|
|
4739
|
+
{
|
|
4740
|
+
isOpen,
|
|
4741
|
+
onClose,
|
|
4742
|
+
title: t.modals.gamepad.title,
|
|
4743
|
+
subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
|
|
4744
|
+
icon: /* @__PURE__ */ jsx(Joystick, { size: 24, style: { color: systemColor } }),
|
|
4745
|
+
systemColor,
|
|
4746
|
+
closeOnBackdrop: !isListening,
|
|
4747
|
+
footer: gamepads.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4903
4748
|
/* @__PURE__ */ jsxs(
|
|
4904
4749
|
"button",
|
|
4905
4750
|
{
|
|
@@ -4925,9 +4770,101 @@ function GamepadMapper({
|
|
|
4925
4770
|
]
|
|
4926
4771
|
}
|
|
4927
4772
|
)
|
|
4928
|
-
] })
|
|
4929
|
-
|
|
4930
|
-
|
|
4773
|
+
] }) : void 0,
|
|
4774
|
+
children: [
|
|
4775
|
+
gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4776
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4777
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4778
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
|
|
4779
|
+
"button",
|
|
4780
|
+
{
|
|
4781
|
+
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4782
|
+
className: `
|
|
4783
|
+
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4784
|
+
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4785
|
+
`,
|
|
4786
|
+
style: selectedPlayer === gp.index + 1 ? {
|
|
4787
|
+
backgroundColor: `${systemColor}20`,
|
|
4788
|
+
color: systemColor
|
|
4789
|
+
} : {},
|
|
4790
|
+
children: [
|
|
4791
|
+
/* @__PURE__ */ jsx(User, { size: 14 }),
|
|
4792
|
+
"P",
|
|
4793
|
+
gp.index + 1
|
|
4794
|
+
]
|
|
4795
|
+
},
|
|
4796
|
+
gp.index
|
|
4797
|
+
)) })
|
|
4798
|
+
] }),
|
|
4799
|
+
currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4800
|
+
] }),
|
|
4801
|
+
gamepads.length === 1 && currentGamepad && /* @__PURE__ */ jsx("div", { className: "px-6 py-2 border-b border-white/10 bg-black/30", children: /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-400", children: [
|
|
4802
|
+
currentGamepad.name,
|
|
4803
|
+
" \u2022 Player 1"
|
|
4804
|
+
] }) }),
|
|
4805
|
+
gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4806
|
+
/* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4807
|
+
/* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4808
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4809
|
+
] }),
|
|
4810
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4811
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4812
|
+
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4813
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4814
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
4815
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4816
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4817
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4818
|
+
] })
|
|
4819
|
+
] })
|
|
4820
|
+
] }),
|
|
4821
|
+
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4822
|
+
listeningFor && /* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg bg-black/50 border border-retro-primary/50 text-center animate-pulse", style: { borderColor: `${systemColor}50` }, children: [
|
|
4823
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4824
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4825
|
+
] }),
|
|
4826
|
+
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4827
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4828
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4829
|
+
"button",
|
|
4830
|
+
{
|
|
4831
|
+
onClick: () => startListening(btn),
|
|
4832
|
+
disabled: !!listeningFor && listeningFor !== btn,
|
|
4833
|
+
className: `
|
|
4834
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4835
|
+
disabled:opacity-50
|
|
4836
|
+
${listeningFor === btn ? "border-retro-primary bg-retro-primary/20 ring-2 ring-retro-primary/50" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4837
|
+
`,
|
|
4838
|
+
style: listeningFor === btn ? {
|
|
4839
|
+
borderColor: systemColor,
|
|
4840
|
+
backgroundColor: `${systemColor}20`,
|
|
4841
|
+
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4842
|
+
} : {},
|
|
4843
|
+
children: [
|
|
4844
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4845
|
+
/* @__PURE__ */ jsx(
|
|
4846
|
+
"span",
|
|
4847
|
+
{
|
|
4848
|
+
className: `
|
|
4849
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4850
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4851
|
+
`,
|
|
4852
|
+
style: listeningFor === btn ? {
|
|
4853
|
+
backgroundColor: `${systemColor}30`,
|
|
4854
|
+
color: systemColor
|
|
4855
|
+
} : {},
|
|
4856
|
+
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4857
|
+
}
|
|
4858
|
+
)
|
|
4859
|
+
]
|
|
4860
|
+
},
|
|
4861
|
+
btn
|
|
4862
|
+
)) })
|
|
4863
|
+
] }, group.label))
|
|
4864
|
+
] })
|
|
4865
|
+
]
|
|
4866
|
+
}
|
|
4867
|
+
);
|
|
4931
4868
|
}
|
|
4932
4869
|
function CheatModal({
|
|
4933
4870
|
isOpen,
|
|
@@ -4935,42 +4872,24 @@ function CheatModal({
|
|
|
4935
4872
|
activeCheats,
|
|
4936
4873
|
onToggle,
|
|
4937
4874
|
onClose
|
|
4938
|
-
}) {
|
|
4939
|
-
const t = useKoinTranslation();
|
|
4940
|
-
const [copiedId, setCopiedId] = React2.useState(null);
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
setCopiedId(
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
}
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-white/10 bg-black/50", children: [
|
|
4957
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4958
|
-
/* @__PURE__ */ jsx(Code, { className: "text-purple-400", size: 24 }),
|
|
4959
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
4960
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: t.modals.cheats.title }),
|
|
4961
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()) })
|
|
4962
|
-
] })
|
|
4963
|
-
] }),
|
|
4964
|
-
/* @__PURE__ */ jsx(
|
|
4965
|
-
"button",
|
|
4966
|
-
{
|
|
4967
|
-
onClick: onClose,
|
|
4968
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
4969
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
4970
|
-
}
|
|
4971
|
-
)
|
|
4972
|
-
] }),
|
|
4973
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4875
|
+
}) {
|
|
4876
|
+
const t = useKoinTranslation();
|
|
4877
|
+
const [copiedId, setCopiedId] = React2.useState(null);
|
|
4878
|
+
const handleCopy = async (code, id) => {
|
|
4879
|
+
await navigator.clipboard.writeText(code);
|
|
4880
|
+
setCopiedId(id);
|
|
4881
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
4882
|
+
};
|
|
4883
|
+
return /* @__PURE__ */ jsx(
|
|
4884
|
+
ModalShell,
|
|
4885
|
+
{
|
|
4886
|
+
isOpen,
|
|
4887
|
+
onClose,
|
|
4888
|
+
title: t.modals.cheats.title,
|
|
4889
|
+
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4890
|
+
icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
|
|
4891
|
+
footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: activeCheats.size > 0 ? t.modals.cheats.active.replace("{{count}}", activeCheats.size.toString()) : t.modals.cheats.toggleHint }),
|
|
4892
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: cheats.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "text-center py-12 text-gray-500", children: [
|
|
4974
4893
|
/* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4975
4894
|
/* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4976
4895
|
/* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4980,18 +4899,18 @@ function CheatModal({
|
|
|
4980
4899
|
"div",
|
|
4981
4900
|
{
|
|
4982
4901
|
className: `
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4902
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
4903
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4904
|
+
`,
|
|
4986
4905
|
onClick: () => onToggle(cheat.id),
|
|
4987
4906
|
children: [
|
|
4988
4907
|
/* @__PURE__ */ jsx(
|
|
4989
4908
|
"div",
|
|
4990
4909
|
{
|
|
4991
4910
|
className: `
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4911
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4912
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4913
|
+
`,
|
|
4995
4914
|
children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
|
|
4996
4915
|
}
|
|
4997
4916
|
),
|
|
@@ -5017,10 +4936,9 @@ function CheatModal({
|
|
|
5017
4936
|
},
|
|
5018
4937
|
cheat.id
|
|
5019
4938
|
);
|
|
5020
|
-
}) })
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
] });
|
|
4939
|
+
}) })
|
|
4940
|
+
}
|
|
4941
|
+
);
|
|
5024
4942
|
}
|
|
5025
4943
|
var AUTO_SAVE_SLOT = 5;
|
|
5026
4944
|
function formatBytes(bytes) {
|
|
@@ -5061,7 +4979,6 @@ function SaveSlotModal({
|
|
|
5061
4979
|
onUpgrade
|
|
5062
4980
|
}) {
|
|
5063
4981
|
const t = useKoinTranslation();
|
|
5064
|
-
if (!isOpen) return null;
|
|
5065
4982
|
const isSaveMode = mode === "save";
|
|
5066
4983
|
const allSlots = [1, 2, 3, 4, 5];
|
|
5067
4984
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -5076,33 +4993,17 @@ function SaveSlotModal({
|
|
|
5076
4993
|
const getSlotData = (slotNum) => {
|
|
5077
4994
|
return slots.find((s) => s.slot === slotNum);
|
|
5078
4995
|
};
|
|
5079
|
-
return /* @__PURE__ */
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
/* @__PURE__ */
|
|
5089
|
-
|
|
5090
|
-
isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
|
|
5091
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
5092
|
-
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold text-white", children: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle }),
|
|
5093
|
-
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad })
|
|
5094
|
-
] })
|
|
5095
|
-
] }),
|
|
5096
|
-
/* @__PURE__ */ jsx(
|
|
5097
|
-
"button",
|
|
5098
|
-
{
|
|
5099
|
-
onClick: onClose,
|
|
5100
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5101
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
5102
|
-
}
|
|
5103
|
-
)
|
|
5104
|
-
] }),
|
|
5105
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
4996
|
+
return /* @__PURE__ */ jsx(
|
|
4997
|
+
ModalShell,
|
|
4998
|
+
{
|
|
4999
|
+
isOpen,
|
|
5000
|
+
onClose,
|
|
5001
|
+
title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
|
|
5002
|
+
subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
|
|
5003
|
+
icon: isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
|
|
5004
|
+
maxWidth: "md",
|
|
5005
|
+
footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
|
|
5006
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2 max-h-[400px] overflow-y-auto", children: isLoading ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-gray-400", children: [
|
|
5106
5007
|
/* @__PURE__ */ jsx(Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
5107
5008
|
/* @__PURE__ */ jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
5108
5009
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5221,10 +5122,9 @@ function SaveSlotModal({
|
|
|
5221
5122
|
},
|
|
5222
5123
|
slotNum
|
|
5223
5124
|
);
|
|
5224
|
-
}) })
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
] });
|
|
5125
|
+
}) })
|
|
5126
|
+
}
|
|
5127
|
+
);
|
|
5228
5128
|
}
|
|
5229
5129
|
function BiosSelectionModal({
|
|
5230
5130
|
isOpen,
|
|
@@ -5371,36 +5271,28 @@ function SettingsModal({
|
|
|
5371
5271
|
systemColor = "#00FF41"
|
|
5372
5272
|
}) {
|
|
5373
5273
|
const t = useKoinTranslation();
|
|
5374
|
-
if (!isOpen) return null;
|
|
5375
5274
|
const languages = [
|
|
5376
5275
|
{ code: "en", name: "English" },
|
|
5377
5276
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5378
5277
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5379
5278
|
];
|
|
5380
|
-
return /* @__PURE__ */
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
}
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
onClick: onClose,
|
|
5398
|
-
className: "p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors",
|
|
5399
|
-
children: /* @__PURE__ */ jsx(X, { size: 20 })
|
|
5400
|
-
}
|
|
5401
|
-
)
|
|
5402
|
-
] }),
|
|
5403
|
-
/* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5279
|
+
return /* @__PURE__ */ jsx(
|
|
5280
|
+
ModalShell,
|
|
5281
|
+
{
|
|
5282
|
+
isOpen,
|
|
5283
|
+
onClose,
|
|
5284
|
+
title: t.settings.title,
|
|
5285
|
+
icon: /* @__PURE__ */ jsx(Settings, { size: 20, className: "text-white" }),
|
|
5286
|
+
maxWidth: "sm",
|
|
5287
|
+
footer: /* @__PURE__ */ jsx(
|
|
5288
|
+
"button",
|
|
5289
|
+
{
|
|
5290
|
+
onClick: onClose,
|
|
5291
|
+
className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
|
|
5292
|
+
children: t.modals.shortcuts.pressEsc
|
|
5293
|
+
}
|
|
5294
|
+
),
|
|
5295
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5404
5296
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5405
5297
|
/* @__PURE__ */ jsx(Globe, { size: 16 }),
|
|
5406
5298
|
/* @__PURE__ */ jsx("span", { children: t.settings.language })
|
|
@@ -5412,9 +5304,9 @@ function SettingsModal({
|
|
|
5412
5304
|
{
|
|
5413
5305
|
onClick: () => onLanguageChange(lang.code),
|
|
5414
5306
|
className: `
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5307
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5308
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5309
|
+
`,
|
|
5418
5310
|
children: [
|
|
5419
5311
|
/* @__PURE__ */ jsx("span", { children: lang.name }),
|
|
5420
5312
|
isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5423,17 +5315,9 @@ function SettingsModal({
|
|
|
5423
5315
|
lang.code
|
|
5424
5316
|
);
|
|
5425
5317
|
}) })
|
|
5426
|
-
] }) })
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
{
|
|
5430
|
-
onClick: onClose,
|
|
5431
|
-
className: "text-sm text-gray-500 hover:text-white transition-colors",
|
|
5432
|
-
children: t.modals.shortcuts.pressEsc
|
|
5433
|
-
}
|
|
5434
|
-
) })
|
|
5435
|
-
] })
|
|
5436
|
-
] });
|
|
5318
|
+
] }) })
|
|
5319
|
+
}
|
|
5320
|
+
);
|
|
5437
5321
|
}
|
|
5438
5322
|
function GameModals({
|
|
5439
5323
|
controlsModalOpen,
|
|
@@ -7644,6 +7528,171 @@ var useNostalgist = ({
|
|
|
7644
7528
|
]);
|
|
7645
7529
|
return hookReturn;
|
|
7646
7530
|
};
|
|
7531
|
+
function getDisplayName(id) {
|
|
7532
|
+
let name = id;
|
|
7533
|
+
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
7534
|
+
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
7535
|
+
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
7536
|
+
if (/xbox/i.test(name)) {
|
|
7537
|
+
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
7538
|
+
if (/one/i.test(name)) return "Xbox One Controller";
|
|
7539
|
+
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
7540
|
+
return "Xbox Controller";
|
|
7541
|
+
}
|
|
7542
|
+
if (/dualsense/i.test(name)) return "DualSense";
|
|
7543
|
+
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
7544
|
+
if (/dualshock/i.test(name)) return "DualShock";
|
|
7545
|
+
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
7546
|
+
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
7547
|
+
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
7548
|
+
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
7549
|
+
return name.trim() || "Gamepad";
|
|
7550
|
+
}
|
|
7551
|
+
function detectControllerBrand(id) {
|
|
7552
|
+
const lowerId = id.toLowerCase();
|
|
7553
|
+
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
7554
|
+
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
7555
|
+
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
7556
|
+
return "generic";
|
|
7557
|
+
}
|
|
7558
|
+
function toGamepadInfo(gamepad) {
|
|
7559
|
+
return {
|
|
7560
|
+
index: gamepad.index,
|
|
7561
|
+
id: gamepad.id,
|
|
7562
|
+
name: getDisplayName(gamepad.id),
|
|
7563
|
+
connected: gamepad.connected,
|
|
7564
|
+
buttons: gamepad.buttons.length,
|
|
7565
|
+
axes: gamepad.axes.length,
|
|
7566
|
+
mapping: gamepad.mapping
|
|
7567
|
+
};
|
|
7568
|
+
}
|
|
7569
|
+
function useGamepad(options) {
|
|
7570
|
+
const { onConnect, onDisconnect } = options || {};
|
|
7571
|
+
const [gamepads, setGamepads] = useState([]);
|
|
7572
|
+
const rafRef = useRef(null);
|
|
7573
|
+
const lastStateRef = useRef("");
|
|
7574
|
+
const prevCountRef = useRef(0);
|
|
7575
|
+
const onConnectRef = useRef(onConnect);
|
|
7576
|
+
const onDisconnectRef = useRef(onDisconnect);
|
|
7577
|
+
useEffect(() => {
|
|
7578
|
+
onConnectRef.current = onConnect;
|
|
7579
|
+
onDisconnectRef.current = onDisconnect;
|
|
7580
|
+
}, [onConnect, onDisconnect]);
|
|
7581
|
+
const getGamepads = useCallback(() => {
|
|
7582
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
7583
|
+
return [];
|
|
7584
|
+
}
|
|
7585
|
+
const rawGamepads = navigator.getGamepads() ?? [];
|
|
7586
|
+
const connected = [];
|
|
7587
|
+
for (let i = 0; i < rawGamepads.length; i++) {
|
|
7588
|
+
const gp = rawGamepads[i];
|
|
7589
|
+
if (gp && gp.connected) {
|
|
7590
|
+
connected.push(toGamepadInfo(gp));
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
return connected;
|
|
7594
|
+
}, []);
|
|
7595
|
+
const getRawGamepad = useCallback((index) => {
|
|
7596
|
+
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
7597
|
+
return rawGamepads[index] ?? null;
|
|
7598
|
+
}, []);
|
|
7599
|
+
const refresh = useCallback(() => {
|
|
7600
|
+
setGamepads(getGamepads());
|
|
7601
|
+
}, [getGamepads]);
|
|
7602
|
+
useEffect(() => {
|
|
7603
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
7604
|
+
return;
|
|
7605
|
+
}
|
|
7606
|
+
if (typeof navigator.getGamepads !== "function") {
|
|
7607
|
+
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
let isActive = true;
|
|
7611
|
+
const poll = () => {
|
|
7612
|
+
if (!isActive) return;
|
|
7613
|
+
const current = getGamepads();
|
|
7614
|
+
let hasChanged = current.length !== prevCountRef.current;
|
|
7615
|
+
if (!hasChanged) {
|
|
7616
|
+
for (let i = 0; i < current.length; i++) {
|
|
7617
|
+
const saved = gamepads[i];
|
|
7618
|
+
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
7619
|
+
hasChanged = true;
|
|
7620
|
+
break;
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
if (hasChanged) {
|
|
7625
|
+
const prevCount = prevCountRef.current;
|
|
7626
|
+
const currentCount = current.length;
|
|
7627
|
+
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
7628
|
+
const newGamepad = current[current.length - 1];
|
|
7629
|
+
onConnectRef.current(newGamepad);
|
|
7630
|
+
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
7631
|
+
onDisconnectRef.current();
|
|
7632
|
+
}
|
|
7633
|
+
prevCountRef.current = currentCount;
|
|
7634
|
+
setGamepads(current);
|
|
7635
|
+
}
|
|
7636
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7637
|
+
};
|
|
7638
|
+
const handleConnect = (e) => {
|
|
7639
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
7640
|
+
const current = getGamepads();
|
|
7641
|
+
const prevCount = prevCountRef.current;
|
|
7642
|
+
prevCountRef.current = current.length;
|
|
7643
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7644
|
+
setGamepads(current);
|
|
7645
|
+
if (onConnectRef.current && current.length > prevCount) {
|
|
7646
|
+
const newGamepad = current[current.length - 1];
|
|
7647
|
+
onConnectRef.current(newGamepad);
|
|
7648
|
+
}
|
|
7649
|
+
};
|
|
7650
|
+
const handleDisconnect = (e) => {
|
|
7651
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
7652
|
+
const current = getGamepads();
|
|
7653
|
+
const prevCount = prevCountRef.current;
|
|
7654
|
+
prevCountRef.current = current.length;
|
|
7655
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7656
|
+
setGamepads(current);
|
|
7657
|
+
if (onDisconnectRef.current && current.length < prevCount) {
|
|
7658
|
+
onDisconnectRef.current();
|
|
7659
|
+
}
|
|
7660
|
+
};
|
|
7661
|
+
window.addEventListener("gamepadconnected", handleConnect);
|
|
7662
|
+
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
7663
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7664
|
+
const initial = getGamepads();
|
|
7665
|
+
if (initial.length > 0) {
|
|
7666
|
+
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
7667
|
+
prevCountRef.current = initial.length;
|
|
7668
|
+
setGamepads(initial);
|
|
7669
|
+
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7670
|
+
} else {
|
|
7671
|
+
prevCountRef.current = 0;
|
|
7672
|
+
}
|
|
7673
|
+
return () => {
|
|
7674
|
+
isActive = false;
|
|
7675
|
+
if (rafRef.current) {
|
|
7676
|
+
cancelAnimationFrame(rafRef.current);
|
|
7677
|
+
}
|
|
7678
|
+
window.removeEventListener("gamepadconnected", handleConnect);
|
|
7679
|
+
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
7680
|
+
};
|
|
7681
|
+
}, [getGamepads]);
|
|
7682
|
+
return {
|
|
7683
|
+
gamepads,
|
|
7684
|
+
isAnyConnected: gamepads.length > 0,
|
|
7685
|
+
connectedCount: gamepads.length,
|
|
7686
|
+
getRawGamepad,
|
|
7687
|
+
refresh
|
|
7688
|
+
};
|
|
7689
|
+
}
|
|
7690
|
+
var STANDARD_AXIS_MAP = {
|
|
7691
|
+
leftStickX: 0,
|
|
7692
|
+
leftStickY: 1,
|
|
7693
|
+
rightStickX: 2,
|
|
7694
|
+
rightStickY: 3
|
|
7695
|
+
};
|
|
7647
7696
|
function useVolume({
|
|
7648
7697
|
setVolume: setVolumeInHook,
|
|
7649
7698
|
toggleMute: toggleMuteInHook
|
|
@@ -9531,27 +9580,15 @@ function AchievementPopup({
|
|
|
9531
9580
|
onDismiss,
|
|
9532
9581
|
autoDismissMs = 5e3
|
|
9533
9582
|
}) {
|
|
9534
|
-
const
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
});
|
|
9540
|
-
const timer = setTimeout(() => {
|
|
9541
|
-
handleDismiss();
|
|
9542
|
-
}, autoDismissMs);
|
|
9543
|
-
return () => clearTimeout(timer);
|
|
9544
|
-
}, [autoDismissMs]);
|
|
9545
|
-
const handleDismiss = () => {
|
|
9546
|
-
setIsExiting(true);
|
|
9547
|
-
setTimeout(() => {
|
|
9548
|
-
onDismiss();
|
|
9549
|
-
}, 300);
|
|
9550
|
-
};
|
|
9583
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9584
|
+
exitDuration: 300,
|
|
9585
|
+
onExit: onDismiss,
|
|
9586
|
+
autoDismissMs
|
|
9587
|
+
});
|
|
9551
9588
|
return /* @__PURE__ */ jsxs(
|
|
9552
9589
|
"div",
|
|
9553
9590
|
{
|
|
9554
|
-
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${
|
|
9591
|
+
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
|
|
9555
9592
|
children: [
|
|
9556
9593
|
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
|
|
9557
9594
|
/* @__PURE__ */ jsx("div", { className: "relative bg-gradient-to-r from-yellow-500 to-orange-500 p-[2px] rounded-lg", children: /* @__PURE__ */ jsxs("div", { className: "bg-gray-900 rounded-lg p-4 flex items-center gap-4 min-w-[320px]", children: [
|
|
@@ -9588,7 +9625,7 @@ function AchievementPopup({
|
|
|
9588
9625
|
/* @__PURE__ */ jsx(
|
|
9589
9626
|
"button",
|
|
9590
9627
|
{
|
|
9591
|
-
onClick:
|
|
9628
|
+
onClick: triggerExit,
|
|
9592
9629
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9593
9630
|
children: /* @__PURE__ */ jsx(X, { size: 18 })
|
|
9594
9631
|
}
|