koin.js 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +698 -645
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +699 -646
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.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';
|
|
@@ -821,7 +821,7 @@ var SaveLoadControls = memo(function SaveLoadControls2({
|
|
|
821
821
|
{
|
|
822
822
|
progress: autoSaveProgress,
|
|
823
823
|
state: autoSavePaused ? "idle" : autoSaveState,
|
|
824
|
-
intervalSeconds:
|
|
824
|
+
intervalSeconds: 60,
|
|
825
825
|
isPaused: autoSavePaused,
|
|
826
826
|
onClick: onAutoSaveToggle
|
|
827
827
|
}
|
|
@@ -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,
|
|
@@ -3629,14 +3701,22 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3629
3701
|
if (!touch) return;
|
|
3630
3702
|
if (drag.isDragging) {
|
|
3631
3703
|
drag.handleDragMove(touch.clientX, touch.clientY);
|
|
3704
|
+
} else if (onPositionChange) {
|
|
3705
|
+
const startedDrag = drag.checkMoveThreshold(touch.clientX, touch.clientY);
|
|
3706
|
+
if (!startedDrag) {
|
|
3707
|
+
drag.clearDragTimer();
|
|
3708
|
+
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3709
|
+
if (rect) {
|
|
3710
|
+
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3632
3713
|
} else {
|
|
3633
3714
|
const rect = dpadRef.current?.getBoundingClientRect();
|
|
3634
3715
|
if (rect) {
|
|
3635
|
-
drag.clearDragTimer();
|
|
3636
3716
|
updateDirections(getDirectionsFromTouch(touch.clientX, touch.clientY, rect));
|
|
3637
3717
|
}
|
|
3638
3718
|
}
|
|
3639
|
-
}, [drag, getDirectionsFromTouch, updateDirections]);
|
|
3719
|
+
}, [drag, getDirectionsFromTouch, updateDirections, onPositionChange]);
|
|
3640
3720
|
const handleTouchEnd = useCallback((e) => {
|
|
3641
3721
|
e.preventDefault();
|
|
3642
3722
|
drag.clearDragTimer();
|
|
@@ -3661,21 +3741,12 @@ var Dpad = React2.memo(function Dpad2({
|
|
|
3661
3741
|
}
|
|
3662
3742
|
}
|
|
3663
3743
|
}, [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]);
|
|
3744
|
+
useTouchEvents(dpadRef, {
|
|
3745
|
+
onTouchStart: handleTouchStart,
|
|
3746
|
+
onTouchMove: handleTouchMove,
|
|
3747
|
+
onTouchEnd: handleTouchEnd,
|
|
3748
|
+
onTouchCancel: handleTouchEnd
|
|
3749
|
+
}, { cleanup: drag.clearDragTimer });
|
|
3679
3750
|
const leftPx = displayX / 100 * containerWidth - size / 2;
|
|
3680
3751
|
const topPx = displayY / 100 * containerHeight - size / 2;
|
|
3681
3752
|
const dUp = "M 35,5 L 65,5 L 65,35 L 50,50 L 35,35 Z";
|
|
@@ -3846,8 +3917,16 @@ function ControlsHint({ isVisible }) {
|
|
|
3846
3917
|
children: [
|
|
3847
3918
|
/* @__PURE__ */ jsx("div", { className: "flex justify-center mb-4", children: /* @__PURE__ */ jsx("div", { className: "w-16 h-16 rounded-full bg-green-500/20 border-2 border-green-400 flex items-center justify-center", children: /* @__PURE__ */ jsx(Move, { size: 32, className: "text-green-400" }) }) }),
|
|
3848
3919
|
/* @__PURE__ */ jsx("h3", { className: "text-white text-lg font-bold mb-2", children: "Customize Your Controls" }),
|
|
3849
|
-
/* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-
|
|
3850
|
-
|
|
3920
|
+
/* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3921
|
+
"Use the ",
|
|
3922
|
+
/* @__PURE__ */ jsx(Lock, { size: 12, className: "inline mx-1 text-white" }),
|
|
3923
|
+
" ",
|
|
3924
|
+
/* @__PURE__ */ jsx("strong", { className: "text-white", children: "lock icon" }),
|
|
3925
|
+
" at the top to unlock controls for repositioning."
|
|
3926
|
+
] }),
|
|
3927
|
+
/* @__PURE__ */ jsxs("p", { className: "text-white/70 text-sm mb-3", children: [
|
|
3928
|
+
"When unlocked, ",
|
|
3929
|
+
/* @__PURE__ */ jsx("strong", { className: "text-white", children: "long-press" }),
|
|
3851
3930
|
" any button or the ",
|
|
3852
3931
|
/* @__PURE__ */ jsx("strong", { className: "text-white", children: "D-pad center" }),
|
|
3853
3932
|
" to drag and reposition it."
|
|
@@ -3875,6 +3954,33 @@ function ControlsHint({ isVisible }) {
|
|
|
3875
3954
|
}
|
|
3876
3955
|
);
|
|
3877
3956
|
}
|
|
3957
|
+
function LockButton({
|
|
3958
|
+
isLocked,
|
|
3959
|
+
onToggle,
|
|
3960
|
+
systemColor = "#00FF41"
|
|
3961
|
+
}) {
|
|
3962
|
+
const Icon = isLocked ? Lock : Unlock;
|
|
3963
|
+
return /* @__PURE__ */ jsx(
|
|
3964
|
+
"button",
|
|
3965
|
+
{
|
|
3966
|
+
onClick: onToggle,
|
|
3967
|
+
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",
|
|
3968
|
+
style: {
|
|
3969
|
+
backgroundColor: isLocked ? "rgba(0,0,0,0.6)" : `${systemColor}20`,
|
|
3970
|
+
border: `1px solid ${isLocked ? "rgba(255,255,255,0.2)" : systemColor}`
|
|
3971
|
+
},
|
|
3972
|
+
"aria-label": isLocked ? "Unlock controls for repositioning" : "Lock controls",
|
|
3973
|
+
children: /* @__PURE__ */ jsx(
|
|
3974
|
+
Icon,
|
|
3975
|
+
{
|
|
3976
|
+
size: 18,
|
|
3977
|
+
style: { color: isLocked ? "rgba(255,255,255,0.6)" : systemColor }
|
|
3978
|
+
}
|
|
3979
|
+
)
|
|
3980
|
+
}
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
var LOCK_KEY = "koin-controls-locked";
|
|
3878
3984
|
function VirtualController({
|
|
3879
3985
|
system,
|
|
3880
3986
|
isRunning,
|
|
@@ -3886,7 +3992,21 @@ function VirtualController({
|
|
|
3886
3992
|
const [pressedButtons, setPressedButtons] = useState(/* @__PURE__ */ new Set());
|
|
3887
3993
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
3888
3994
|
const [isFullscreenState, setIsFullscreenState] = useState(false);
|
|
3995
|
+
const [isLocked, setIsLocked] = useState(true);
|
|
3889
3996
|
const { getPosition, savePosition } = useButtonPositions();
|
|
3997
|
+
useEffect(() => {
|
|
3998
|
+
const stored = localStorage.getItem(LOCK_KEY);
|
|
3999
|
+
if (stored !== null) {
|
|
4000
|
+
setIsLocked(stored === "true");
|
|
4001
|
+
}
|
|
4002
|
+
}, []);
|
|
4003
|
+
const toggleLock = useCallback(() => {
|
|
4004
|
+
setIsLocked((prev) => {
|
|
4005
|
+
const newValue = !prev;
|
|
4006
|
+
localStorage.setItem(LOCK_KEY, String(newValue));
|
|
4007
|
+
return newValue;
|
|
4008
|
+
});
|
|
4009
|
+
}, []);
|
|
3890
4010
|
const layout = getLayoutForSystem(system);
|
|
3891
4011
|
const visibleButtons = layout.buttons.filter((btn) => {
|
|
3892
4012
|
if (isPortrait) {
|
|
@@ -4041,6 +4161,14 @@ function VirtualController({
|
|
|
4041
4161
|
className: "fixed inset-0 z-30 pointer-events-none",
|
|
4042
4162
|
style: { touchAction: "none" },
|
|
4043
4163
|
children: [
|
|
4164
|
+
/* @__PURE__ */ jsx(
|
|
4165
|
+
LockButton,
|
|
4166
|
+
{
|
|
4167
|
+
isLocked,
|
|
4168
|
+
onToggle: toggleLock,
|
|
4169
|
+
systemColor
|
|
4170
|
+
}
|
|
4171
|
+
),
|
|
4044
4172
|
/* @__PURE__ */ jsx(
|
|
4045
4173
|
Dpad_default,
|
|
4046
4174
|
{
|
|
@@ -4053,7 +4181,7 @@ function VirtualController({
|
|
|
4053
4181
|
systemColor,
|
|
4054
4182
|
isLandscape,
|
|
4055
4183
|
customPosition: getPosition("up", isLandscape),
|
|
4056
|
-
onPositionChange: (x, y) => savePosition("up", x, y, isLandscape)
|
|
4184
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition("up", x, y, isLandscape)
|
|
4057
4185
|
}
|
|
4058
4186
|
),
|
|
4059
4187
|
memoizedButtonElements.filter(({ buttonConfig }) => !DPAD_TYPES.includes(buttonConfig.type)).map(({ buttonConfig, adjustedConfig, customPosition, width, height }) => /* @__PURE__ */ jsx(
|
|
@@ -4067,7 +4195,7 @@ function VirtualController({
|
|
|
4067
4195
|
containerWidth: width,
|
|
4068
4196
|
containerHeight: height,
|
|
4069
4197
|
customPosition,
|
|
4070
|
-
onPositionChange: (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4198
|
+
onPositionChange: isLocked ? void 0 : (x, y) => savePosition(buttonConfig.type, x, y, isLandscape),
|
|
4071
4199
|
isLandscape,
|
|
4072
4200
|
console: layout.console
|
|
4073
4201
|
},
|
|
@@ -4387,6 +4515,45 @@ var GameCanvas = memo(function GameCanvas2({
|
|
|
4387
4515
|
] });
|
|
4388
4516
|
});
|
|
4389
4517
|
var GameCanvas_default = GameCanvas;
|
|
4518
|
+
function useInputCapture({
|
|
4519
|
+
isOpen,
|
|
4520
|
+
onClose
|
|
4521
|
+
}) {
|
|
4522
|
+
const [listeningFor, setListeningFor] = useState(null);
|
|
4523
|
+
const startListening = useCallback((target) => {
|
|
4524
|
+
setListeningFor(target);
|
|
4525
|
+
}, []);
|
|
4526
|
+
const stopListening = useCallback(() => {
|
|
4527
|
+
setListeningFor(null);
|
|
4528
|
+
}, []);
|
|
4529
|
+
useEffect(() => {
|
|
4530
|
+
if (!isOpen) {
|
|
4531
|
+
setListeningFor(null);
|
|
4532
|
+
}
|
|
4533
|
+
}, [isOpen]);
|
|
4534
|
+
useEffect(() => {
|
|
4535
|
+
if (!isOpen) return;
|
|
4536
|
+
const handleKeyDown = (e) => {
|
|
4537
|
+
if (e.code === "Escape") {
|
|
4538
|
+
if (listeningFor !== null) {
|
|
4539
|
+
e.preventDefault();
|
|
4540
|
+
e.stopPropagation();
|
|
4541
|
+
setListeningFor(null);
|
|
4542
|
+
} else {
|
|
4543
|
+
onClose();
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4548
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4549
|
+
}, [isOpen, listeningFor, onClose]);
|
|
4550
|
+
return {
|
|
4551
|
+
listeningFor,
|
|
4552
|
+
startListening,
|
|
4553
|
+
stopListening,
|
|
4554
|
+
isListening: listeningFor !== null
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4390
4557
|
function getFilteredGroups(activeButtons) {
|
|
4391
4558
|
return BUTTON_GROUPS.map((group) => ({
|
|
4392
4559
|
...group,
|
|
@@ -4402,7 +4569,10 @@ function ControlMapper({
|
|
|
4402
4569
|
}) {
|
|
4403
4570
|
const t = useKoinTranslation();
|
|
4404
4571
|
const [localControls, setLocalControls] = useState(controls);
|
|
4405
|
-
const
|
|
4572
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4573
|
+
isOpen,
|
|
4574
|
+
onClose
|
|
4575
|
+
});
|
|
4406
4576
|
const activeButtons = useMemo(() => {
|
|
4407
4577
|
return getConsoleButtons(system || "SNES");
|
|
4408
4578
|
}, [system]);
|
|
@@ -4418,27 +4588,20 @@ function ControlMapper({
|
|
|
4418
4588
|
}
|
|
4419
4589
|
}, [isOpen, controls]);
|
|
4420
4590
|
useEffect(() => {
|
|
4421
|
-
if (!isOpen)
|
|
4422
|
-
setListeningFor(null);
|
|
4423
|
-
return;
|
|
4424
|
-
}
|
|
4591
|
+
if (!isOpen || !listeningFor) return;
|
|
4425
4592
|
const handleKeyDown = (e) => {
|
|
4426
|
-
if (
|
|
4593
|
+
if (e.code === "Escape") return;
|
|
4427
4594
|
e.preventDefault();
|
|
4428
4595
|
e.stopPropagation();
|
|
4429
|
-
if (e.code === "Escape") {
|
|
4430
|
-
setListeningFor(null);
|
|
4431
|
-
return;
|
|
4432
|
-
}
|
|
4433
4596
|
setLocalControls((prev) => ({
|
|
4434
4597
|
...prev,
|
|
4435
4598
|
[listeningFor]: e.code
|
|
4436
4599
|
}));
|
|
4437
|
-
|
|
4600
|
+
stopListening();
|
|
4438
4601
|
};
|
|
4439
4602
|
window.addEventListener("keydown", handleKeyDown);
|
|
4440
4603
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
4441
|
-
}, [isOpen, listeningFor]);
|
|
4604
|
+
}, [isOpen, listeningFor, stopListening]);
|
|
4442
4605
|
const handleReset = () => {
|
|
4443
4606
|
setLocalControls(defaultControls);
|
|
4444
4607
|
};
|
|
@@ -4446,52 +4609,58 @@ function ControlMapper({
|
|
|
4446
4609
|
onSave(localControls);
|
|
4447
4610
|
onClose();
|
|
4448
4611
|
};
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4612
|
+
return /* @__PURE__ */ jsx(
|
|
4613
|
+
ModalShell,
|
|
4614
|
+
{
|
|
4615
|
+
isOpen,
|
|
4616
|
+
onClose,
|
|
4617
|
+
title: t.modals.controls.title,
|
|
4618
|
+
subtitle: t.modals.controls.description,
|
|
4619
|
+
icon: /* @__PURE__ */ jsx(Gamepad2, { className: "text-retro-primary", size: 24 }),
|
|
4620
|
+
closeOnBackdrop: !isListening,
|
|
4621
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4622
|
+
/* @__PURE__ */ jsxs(
|
|
4623
|
+
"button",
|
|
4624
|
+
{
|
|
4625
|
+
onClick: handleReset,
|
|
4626
|
+
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",
|
|
4627
|
+
children: [
|
|
4628
|
+
/* @__PURE__ */ jsx(RotateCcw, { size: 16 }),
|
|
4629
|
+
t.modals.controls.reset
|
|
4630
|
+
]
|
|
4631
|
+
}
|
|
4632
|
+
),
|
|
4633
|
+
/* @__PURE__ */ jsxs(
|
|
4468
4634
|
"button",
|
|
4469
4635
|
{
|
|
4470
|
-
onClick:
|
|
4471
|
-
className: "
|
|
4472
|
-
children:
|
|
4636
|
+
onClick: handleSave,
|
|
4637
|
+
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",
|
|
4638
|
+
children: [
|
|
4639
|
+
/* @__PURE__ */ jsx(Check, { size: 16 }),
|
|
4640
|
+
t.modals.controls.save
|
|
4641
|
+
]
|
|
4473
4642
|
}
|
|
4474
4643
|
)
|
|
4475
4644
|
] }),
|
|
4476
|
-
/* @__PURE__ */ jsx("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: controlGroups.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4645
|
+
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
4646
|
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4478
4647
|
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4479
4648
|
"button",
|
|
4480
4649
|
{
|
|
4481
|
-
onClick: () =>
|
|
4650
|
+
onClick: () => startListening(btn),
|
|
4482
4651
|
className: `
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4652
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4653
|
+
${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"}
|
|
4654
|
+
`,
|
|
4486
4655
|
children: [
|
|
4487
4656
|
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4488
4657
|
/* @__PURE__ */ jsx(
|
|
4489
4658
|
"span",
|
|
4490
4659
|
{
|
|
4491
4660
|
className: `
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4661
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4662
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4663
|
+
`,
|
|
4495
4664
|
children: listeningFor === btn ? t.modals.controls.pressKey : formatKeyCode(localControls[btn] || "")
|
|
4496
4665
|
}
|
|
4497
4666
|
)
|
|
@@ -4499,199 +4668,10 @@ function ControlMapper({
|
|
|
4499
4668
|
},
|
|
4500
4669
|
btn
|
|
4501
4670
|
)) })
|
|
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 [];
|
|
4583
|
-
}
|
|
4584
|
-
const rawGamepads = navigator.getGamepads() ?? [];
|
|
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
|
+
] }, group.label)) })
|
|
4671
4672
|
}
|
|
4672
|
-
|
|
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
|
-
};
|
|
4673
|
+
);
|
|
4688
4674
|
}
|
|
4689
|
-
var STANDARD_AXIS_MAP = {
|
|
4690
|
-
leftStickX: 0,
|
|
4691
|
-
leftStickY: 1,
|
|
4692
|
-
rightStickX: 2,
|
|
4693
|
-
rightStickY: 3
|
|
4694
|
-
};
|
|
4695
4675
|
function GamepadMapper({
|
|
4696
4676
|
isOpen,
|
|
4697
4677
|
gamepads,
|
|
@@ -4702,7 +4682,10 @@ function GamepadMapper({
|
|
|
4702
4682
|
const t = useKoinTranslation();
|
|
4703
4683
|
const [selectedPlayer, setSelectedPlayer] = useState(1);
|
|
4704
4684
|
const [bindings, setBindings] = useState({});
|
|
4705
|
-
const
|
|
4685
|
+
const { listeningFor, startListening, stopListening, isListening } = useInputCapture({
|
|
4686
|
+
isOpen,
|
|
4687
|
+
onClose
|
|
4688
|
+
});
|
|
4706
4689
|
const rafRef = useRef(null);
|
|
4707
4690
|
useEffect(() => {
|
|
4708
4691
|
if (isOpen) {
|
|
@@ -4737,7 +4720,7 @@ function GamepadMapper({
|
|
|
4737
4720
|
[listeningFor]: i
|
|
4738
4721
|
}
|
|
4739
4722
|
}));
|
|
4740
|
-
|
|
4723
|
+
stopListening();
|
|
4741
4724
|
return;
|
|
4742
4725
|
}
|
|
4743
4726
|
}
|
|
@@ -4750,21 +4733,7 @@ function GamepadMapper({
|
|
|
4750
4733
|
cancelAnimationFrame(rafRef.current);
|
|
4751
4734
|
}
|
|
4752
4735
|
};
|
|
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]);
|
|
4736
|
+
}, [isOpen, listeningFor, selectedPlayer, stopListening]);
|
|
4768
4737
|
const handleReset = () => {
|
|
4769
4738
|
setBindings((prev) => ({
|
|
4770
4739
|
...prev,
|
|
@@ -4779,127 +4748,19 @@ function GamepadMapper({
|
|
|
4779
4748
|
onSave?.(bindings[selectedPlayer], selectedPlayer);
|
|
4780
4749
|
onClose();
|
|
4781
4750
|
};
|
|
4782
|
-
if (!isOpen) return null;
|
|
4783
4751
|
const currentBindings = bindings[selectedPlayer] ?? DEFAULT_GAMEPAD;
|
|
4784
4752
|
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: [
|
|
4753
|
+
return /* @__PURE__ */ jsxs(
|
|
4754
|
+
ModalShell,
|
|
4755
|
+
{
|
|
4756
|
+
isOpen,
|
|
4757
|
+
onClose,
|
|
4758
|
+
title: t.modals.gamepad.title,
|
|
4759
|
+
subtitle: gamepads.length > 0 ? t.modals.gamepad.connected.replace("{{count}}", gamepads.length.toString()) : t.modals.gamepad.none,
|
|
4760
|
+
icon: /* @__PURE__ */ jsx(Joystick, { size: 24, style: { color: systemColor } }),
|
|
4761
|
+
systemColor,
|
|
4762
|
+
closeOnBackdrop: !isListening,
|
|
4763
|
+
footer: gamepads.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4903
4764
|
/* @__PURE__ */ jsxs(
|
|
4904
4765
|
"button",
|
|
4905
4766
|
{
|
|
@@ -4925,9 +4786,101 @@ function GamepadMapper({
|
|
|
4925
4786
|
]
|
|
4926
4787
|
}
|
|
4927
4788
|
)
|
|
4928
|
-
] })
|
|
4929
|
-
|
|
4930
|
-
|
|
4789
|
+
] }) : void 0,
|
|
4790
|
+
children: [
|
|
4791
|
+
gamepads.length > 1 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-3 border-b border-white/10 bg-black/30", children: [
|
|
4792
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4793
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-medium", children: t.modals.gamepad.player }),
|
|
4794
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1", children: gamepads.map((gp) => /* @__PURE__ */ jsxs(
|
|
4795
|
+
"button",
|
|
4796
|
+
{
|
|
4797
|
+
onClick: () => setSelectedPlayer(gp.index + 1),
|
|
4798
|
+
className: `
|
|
4799
|
+
flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all
|
|
4800
|
+
${selectedPlayer === gp.index + 1 ? "bg-retro-primary/20 text-retro-primary" : "bg-white/5 text-gray-400 hover:bg-white/10 hover:text-white"}
|
|
4801
|
+
`,
|
|
4802
|
+
style: selectedPlayer === gp.index + 1 ? {
|
|
4803
|
+
backgroundColor: `${systemColor}20`,
|
|
4804
|
+
color: systemColor
|
|
4805
|
+
} : {},
|
|
4806
|
+
children: [
|
|
4807
|
+
/* @__PURE__ */ jsx(User, { size: 14 }),
|
|
4808
|
+
"P",
|
|
4809
|
+
gp.index + 1
|
|
4810
|
+
]
|
|
4811
|
+
},
|
|
4812
|
+
gp.index
|
|
4813
|
+
)) })
|
|
4814
|
+
] }),
|
|
4815
|
+
currentGamepad && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: currentGamepad.name })
|
|
4816
|
+
] }),
|
|
4817
|
+
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: [
|
|
4818
|
+
currentGamepad.name,
|
|
4819
|
+
" \u2022 Player 1"
|
|
4820
|
+
] }) }),
|
|
4821
|
+
gamepads.length === 0 && /* @__PURE__ */ jsxs("div", { className: "px-6 py-10 text-center", children: [
|
|
4822
|
+
/* @__PURE__ */ jsxs("div", { className: "relative inline-block mb-4", children: [
|
|
4823
|
+
/* @__PURE__ */ jsx(Joystick, { size: 56, className: "text-gray-600 animate-pulse" }),
|
|
4824
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 -m-2 rounded-full border-2 border-dashed border-gray-600 animate-spin", style: { animationDuration: "8s" } })
|
|
4825
|
+
] }),
|
|
4826
|
+
/* @__PURE__ */ jsx("p", { className: "text-gray-300 font-medium mb-2", children: t.modals.gamepad.noController }),
|
|
4827
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500 mb-4", children: t.modals.gamepad.pressAny }),
|
|
4828
|
+
/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 border border-white/10", children: [
|
|
4829
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: t.modals.gamepad.waiting }),
|
|
4830
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
4831
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "0ms" } }),
|
|
4832
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "150ms" } }),
|
|
4833
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-gray-500 animate-bounce", style: { animationDelay: "300ms" } })
|
|
4834
|
+
] })
|
|
4835
|
+
] })
|
|
4836
|
+
] }),
|
|
4837
|
+
gamepads.length > 0 && /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-6 max-h-[400px] overflow-y-auto", children: [
|
|
4838
|
+
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: [
|
|
4839
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-white mb-1", children: t.modals.gamepad.pressButton.replace("{{button}}", BUTTON_LABELS[listeningFor]) }),
|
|
4840
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: t.modals.gamepad.pressEsc })
|
|
4841
|
+
] }),
|
|
4842
|
+
BUTTON_GROUPS.map((group) => /* @__PURE__ */ jsxs("div", { children: [
|
|
4843
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-bold text-gray-500 uppercase tracking-wider mb-2", children: group.label }),
|
|
4844
|
+
/* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: group.buttons.map((btn) => /* @__PURE__ */ jsxs(
|
|
4845
|
+
"button",
|
|
4846
|
+
{
|
|
4847
|
+
onClick: () => startListening(btn),
|
|
4848
|
+
disabled: !!listeningFor && listeningFor !== btn,
|
|
4849
|
+
className: `
|
|
4850
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
4851
|
+
disabled:opacity-50
|
|
4852
|
+
${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"}
|
|
4853
|
+
`,
|
|
4854
|
+
style: listeningFor === btn ? {
|
|
4855
|
+
borderColor: systemColor,
|
|
4856
|
+
backgroundColor: `${systemColor}20`,
|
|
4857
|
+
boxShadow: `0 0 0 2px ${systemColor}50`
|
|
4858
|
+
} : {},
|
|
4859
|
+
children: [
|
|
4860
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-gray-300", children: BUTTON_LABELS[btn] }),
|
|
4861
|
+
/* @__PURE__ */ jsx(
|
|
4862
|
+
"span",
|
|
4863
|
+
{
|
|
4864
|
+
className: `
|
|
4865
|
+
px-2 py-1 rounded text-xs font-mono
|
|
4866
|
+
${listeningFor === btn ? "bg-retro-primary/30 text-retro-primary animate-pulse" : "bg-black/50 text-white"}
|
|
4867
|
+
`,
|
|
4868
|
+
style: listeningFor === btn ? {
|
|
4869
|
+
backgroundColor: `${systemColor}30`,
|
|
4870
|
+
color: systemColor
|
|
4871
|
+
} : {},
|
|
4872
|
+
children: listeningFor === btn ? t.controls.press : formatGamepadButton(currentBindings[btn])
|
|
4873
|
+
}
|
|
4874
|
+
)
|
|
4875
|
+
]
|
|
4876
|
+
},
|
|
4877
|
+
btn
|
|
4878
|
+
)) })
|
|
4879
|
+
] }, group.label))
|
|
4880
|
+
] })
|
|
4881
|
+
]
|
|
4882
|
+
}
|
|
4883
|
+
);
|
|
4931
4884
|
}
|
|
4932
4885
|
function CheatModal({
|
|
4933
4886
|
isOpen,
|
|
@@ -4935,42 +4888,24 @@ function CheatModal({
|
|
|
4935
4888
|
activeCheats,
|
|
4936
4889
|
onToggle,
|
|
4937
4890
|
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: [
|
|
4891
|
+
}) {
|
|
4892
|
+
const t = useKoinTranslation();
|
|
4893
|
+
const [copiedId, setCopiedId] = React2.useState(null);
|
|
4894
|
+
const handleCopy = async (code, id) => {
|
|
4895
|
+
await navigator.clipboard.writeText(code);
|
|
4896
|
+
setCopiedId(id);
|
|
4897
|
+
setTimeout(() => setCopiedId(null), 2e3);
|
|
4898
|
+
};
|
|
4899
|
+
return /* @__PURE__ */ jsx(
|
|
4900
|
+
ModalShell,
|
|
4901
|
+
{
|
|
4902
|
+
isOpen,
|
|
4903
|
+
onClose,
|
|
4904
|
+
title: t.modals.cheats.title,
|
|
4905
|
+
subtitle: t.modals.cheats.available.replace("{{count}}", cheats.length.toString()),
|
|
4906
|
+
icon: /* @__PURE__ */ jsx(Code, { size: 24, className: "text-purple-400" }),
|
|
4907
|
+
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 }),
|
|
4908
|
+
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
4909
|
/* @__PURE__ */ jsx(Code, { size: 48, className: "mx-auto mb-3 opacity-50" }),
|
|
4975
4910
|
/* @__PURE__ */ jsx("p", { className: "font-medium", children: t.modals.cheats.emptyTitle }),
|
|
4976
4911
|
/* @__PURE__ */ jsx("p", { className: "text-sm mt-1", children: t.modals.cheats.emptyDesc })
|
|
@@ -4980,18 +4915,18 @@ function CheatModal({
|
|
|
4980
4915
|
"div",
|
|
4981
4916
|
{
|
|
4982
4917
|
className: `
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4918
|
+
group flex items-start gap-4 p-4 rounded-lg border transition-all cursor-pointer
|
|
4919
|
+
${isActive ? "border-purple-500/50 bg-purple-500/10" : "border-white/10 bg-white/5 hover:border-white/20 hover:bg-white/10"}
|
|
4920
|
+
`,
|
|
4986
4921
|
onClick: () => onToggle(cheat.id),
|
|
4987
4922
|
children: [
|
|
4988
4923
|
/* @__PURE__ */ jsx(
|
|
4989
4924
|
"div",
|
|
4990
4925
|
{
|
|
4991
4926
|
className: `
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4927
|
+
flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all
|
|
4928
|
+
${isActive ? "border-purple-500 bg-purple-500" : "border-gray-600 group-hover:border-gray-400"}
|
|
4929
|
+
`,
|
|
4995
4930
|
children: isActive && /* @__PURE__ */ jsx(Check, { size: 14, className: "text-white" })
|
|
4996
4931
|
}
|
|
4997
4932
|
),
|
|
@@ -5017,10 +4952,9 @@ function CheatModal({
|
|
|
5017
4952
|
},
|
|
5018
4953
|
cheat.id
|
|
5019
4954
|
);
|
|
5020
|
-
}) })
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
] });
|
|
4955
|
+
}) })
|
|
4956
|
+
}
|
|
4957
|
+
);
|
|
5024
4958
|
}
|
|
5025
4959
|
var AUTO_SAVE_SLOT = 5;
|
|
5026
4960
|
function formatBytes(bytes) {
|
|
@@ -5061,7 +4995,6 @@ function SaveSlotModal({
|
|
|
5061
4995
|
onUpgrade
|
|
5062
4996
|
}) {
|
|
5063
4997
|
const t = useKoinTranslation();
|
|
5064
|
-
if (!isOpen) return null;
|
|
5065
4998
|
const isSaveMode = mode === "save";
|
|
5066
4999
|
const allSlots = [1, 2, 3, 4, 5];
|
|
5067
5000
|
const isUnlimited = maxSlots === -1 || maxSlots >= 5;
|
|
@@ -5076,33 +5009,17 @@ function SaveSlotModal({
|
|
|
5076
5009
|
const getSlotData = (slotNum) => {
|
|
5077
5010
|
return slots.find((s) => s.slot === slotNum);
|
|
5078
5011
|
};
|
|
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: [
|
|
5012
|
+
return /* @__PURE__ */ jsx(
|
|
5013
|
+
ModalShell,
|
|
5014
|
+
{
|
|
5015
|
+
isOpen,
|
|
5016
|
+
onClose,
|
|
5017
|
+
title: isSaveMode ? t.modals.saveSlots.saveTitle : t.modals.saveSlots.loadTitle,
|
|
5018
|
+
subtitle: isSaveMode ? t.modals.saveSlots.subtitleSave : t.modals.saveSlots.subtitleLoad,
|
|
5019
|
+
icon: isSaveMode ? /* @__PURE__ */ jsx(Save, { className: "text-retro-primary", size: 24 }) : /* @__PURE__ */ jsx(Download, { className: "text-retro-primary", size: 24 }),
|
|
5020
|
+
maxWidth: "md",
|
|
5021
|
+
footer: /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 text-center w-full", children: isSaveMode ? t.modals.saveSlots.footerSave : t.modals.saveSlots.footerLoad }),
|
|
5022
|
+
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
5023
|
/* @__PURE__ */ jsx(Loader2, { className: "w-8 h-8 animate-spin mb-3" }),
|
|
5107
5024
|
/* @__PURE__ */ jsx("span", { className: "text-sm", children: t.modals.saveSlots.loading })
|
|
5108
5025
|
] }) : displaySlots.map((slotNum) => {
|
|
@@ -5221,10 +5138,9 @@ function SaveSlotModal({
|
|
|
5221
5138
|
},
|
|
5222
5139
|
slotNum
|
|
5223
5140
|
);
|
|
5224
|
-
}) })
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
] });
|
|
5141
|
+
}) })
|
|
5142
|
+
}
|
|
5143
|
+
);
|
|
5228
5144
|
}
|
|
5229
5145
|
function BiosSelectionModal({
|
|
5230
5146
|
isOpen,
|
|
@@ -5371,36 +5287,28 @@ function SettingsModal({
|
|
|
5371
5287
|
systemColor = "#00FF41"
|
|
5372
5288
|
}) {
|
|
5373
5289
|
const t = useKoinTranslation();
|
|
5374
|
-
if (!isOpen) return null;
|
|
5375
5290
|
const languages = [
|
|
5376
5291
|
{ code: "en", name: "English" },
|
|
5377
5292
|
{ code: "es", name: "Espa\xF1ol" },
|
|
5378
5293
|
{ code: "fr", name: "Fran\xE7ais" }
|
|
5379
5294
|
];
|
|
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: [
|
|
5295
|
+
return /* @__PURE__ */ jsx(
|
|
5296
|
+
ModalShell,
|
|
5297
|
+
{
|
|
5298
|
+
isOpen,
|
|
5299
|
+
onClose,
|
|
5300
|
+
title: t.settings.title,
|
|
5301
|
+
icon: /* @__PURE__ */ jsx(Settings, { size: 20, className: "text-white" }),
|
|
5302
|
+
maxWidth: "sm",
|
|
5303
|
+
footer: /* @__PURE__ */ jsx(
|
|
5304
|
+
"button",
|
|
5305
|
+
{
|
|
5306
|
+
onClick: onClose,
|
|
5307
|
+
className: "text-sm text-gray-500 hover:text-white transition-colors w-full text-center",
|
|
5308
|
+
children: t.modals.shortcuts.pressEsc
|
|
5309
|
+
}
|
|
5310
|
+
),
|
|
5311
|
+
children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5404
5312
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-gray-400", children: [
|
|
5405
5313
|
/* @__PURE__ */ jsx(Globe, { size: 16 }),
|
|
5406
5314
|
/* @__PURE__ */ jsx("span", { children: t.settings.language })
|
|
@@ -5412,9 +5320,9 @@ function SettingsModal({
|
|
|
5412
5320
|
{
|
|
5413
5321
|
onClick: () => onLanguageChange(lang.code),
|
|
5414
5322
|
className: `
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5323
|
+
flex items-center justify-between px-4 py-3 rounded-lg border transition-all
|
|
5324
|
+
${isActive ? "bg-white/10 border-white/20 text-white" : "bg-black/20 border-transparent text-gray-400 hover:bg-white/5 hover:text-white"}
|
|
5325
|
+
`,
|
|
5418
5326
|
children: [
|
|
5419
5327
|
/* @__PURE__ */ jsx("span", { children: lang.name }),
|
|
5420
5328
|
isActive && /* @__PURE__ */ jsx(Check, { size: 16, style: { color: systemColor } })
|
|
@@ -5423,17 +5331,9 @@ function SettingsModal({
|
|
|
5423
5331
|
lang.code
|
|
5424
5332
|
);
|
|
5425
5333
|
}) })
|
|
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
|
-
] });
|
|
5334
|
+
] }) })
|
|
5335
|
+
}
|
|
5336
|
+
);
|
|
5437
5337
|
}
|
|
5438
5338
|
function GameModals({
|
|
5439
5339
|
controlsModalOpen,
|
|
@@ -7644,6 +7544,171 @@ var useNostalgist = ({
|
|
|
7644
7544
|
]);
|
|
7645
7545
|
return hookReturn;
|
|
7646
7546
|
};
|
|
7547
|
+
function getDisplayName(id) {
|
|
7548
|
+
let name = id;
|
|
7549
|
+
name = name.replace(/^[0-9a-f]{4}-[0-9a-f]{4}-/i, "");
|
|
7550
|
+
name = name.replace(/\s*\(.*\)\s*$/i, "");
|
|
7551
|
+
name = name.replace(/\s*STANDARD GAMEPAD\s*/i, "");
|
|
7552
|
+
if (/xbox/i.test(name)) {
|
|
7553
|
+
if (/series/i.test(name)) return "Xbox Series Controller";
|
|
7554
|
+
if (/one/i.test(name)) return "Xbox One Controller";
|
|
7555
|
+
if (/360/i.test(name)) return "Xbox 360 Controller";
|
|
7556
|
+
return "Xbox Controller";
|
|
7557
|
+
}
|
|
7558
|
+
if (/dualsense/i.test(name)) return "DualSense";
|
|
7559
|
+
if (/dualshock\s*4/i.test(name)) return "DualShock 4";
|
|
7560
|
+
if (/dualshock/i.test(name)) return "DualShock";
|
|
7561
|
+
if (/playstation/i.test(name) || /sony/i.test(name)) return "PlayStation Controller";
|
|
7562
|
+
if (/pro\s*controller/i.test(name)) return "Switch Pro Controller";
|
|
7563
|
+
if (/joy-?con/i.test(name)) return "Joy-Con";
|
|
7564
|
+
if (/nintendo/i.test(name)) return "Nintendo Controller";
|
|
7565
|
+
return name.trim() || "Gamepad";
|
|
7566
|
+
}
|
|
7567
|
+
function detectControllerBrand(id) {
|
|
7568
|
+
const lowerId = id.toLowerCase();
|
|
7569
|
+
if (/xbox|xinput|microsoft/i.test(lowerId)) return "xbox";
|
|
7570
|
+
if (/playstation|sony|dualshock|dualsense/i.test(lowerId)) return "playstation";
|
|
7571
|
+
if (/nintendo|switch|joy-?con|pro controller/i.test(lowerId)) return "nintendo";
|
|
7572
|
+
return "generic";
|
|
7573
|
+
}
|
|
7574
|
+
function toGamepadInfo(gamepad) {
|
|
7575
|
+
return {
|
|
7576
|
+
index: gamepad.index,
|
|
7577
|
+
id: gamepad.id,
|
|
7578
|
+
name: getDisplayName(gamepad.id),
|
|
7579
|
+
connected: gamepad.connected,
|
|
7580
|
+
buttons: gamepad.buttons.length,
|
|
7581
|
+
axes: gamepad.axes.length,
|
|
7582
|
+
mapping: gamepad.mapping
|
|
7583
|
+
};
|
|
7584
|
+
}
|
|
7585
|
+
function useGamepad(options) {
|
|
7586
|
+
const { onConnect, onDisconnect } = options || {};
|
|
7587
|
+
const [gamepads, setGamepads] = useState([]);
|
|
7588
|
+
const rafRef = useRef(null);
|
|
7589
|
+
const lastStateRef = useRef("");
|
|
7590
|
+
const prevCountRef = useRef(0);
|
|
7591
|
+
const onConnectRef = useRef(onConnect);
|
|
7592
|
+
const onDisconnectRef = useRef(onDisconnect);
|
|
7593
|
+
useEffect(() => {
|
|
7594
|
+
onConnectRef.current = onConnect;
|
|
7595
|
+
onDisconnectRef.current = onDisconnect;
|
|
7596
|
+
}, [onConnect, onDisconnect]);
|
|
7597
|
+
const getGamepads = useCallback(() => {
|
|
7598
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") {
|
|
7599
|
+
return [];
|
|
7600
|
+
}
|
|
7601
|
+
const rawGamepads = navigator.getGamepads() ?? [];
|
|
7602
|
+
const connected = [];
|
|
7603
|
+
for (let i = 0; i < rawGamepads.length; i++) {
|
|
7604
|
+
const gp = rawGamepads[i];
|
|
7605
|
+
if (gp && gp.connected) {
|
|
7606
|
+
connected.push(toGamepadInfo(gp));
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
return connected;
|
|
7610
|
+
}, []);
|
|
7611
|
+
const getRawGamepad = useCallback((index) => {
|
|
7612
|
+
const rawGamepads = navigator.getGamepads?.() ?? [];
|
|
7613
|
+
return rawGamepads[index] ?? null;
|
|
7614
|
+
}, []);
|
|
7615
|
+
const refresh = useCallback(() => {
|
|
7616
|
+
setGamepads(getGamepads());
|
|
7617
|
+
}, [getGamepads]);
|
|
7618
|
+
useEffect(() => {
|
|
7619
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") {
|
|
7620
|
+
return;
|
|
7621
|
+
}
|
|
7622
|
+
if (typeof navigator.getGamepads !== "function") {
|
|
7623
|
+
console.warn("[useGamepad] Gamepad API not supported in this browser");
|
|
7624
|
+
return;
|
|
7625
|
+
}
|
|
7626
|
+
let isActive = true;
|
|
7627
|
+
const poll = () => {
|
|
7628
|
+
if (!isActive) return;
|
|
7629
|
+
const current = getGamepads();
|
|
7630
|
+
let hasChanged = current.length !== prevCountRef.current;
|
|
7631
|
+
if (!hasChanged) {
|
|
7632
|
+
for (let i = 0; i < current.length; i++) {
|
|
7633
|
+
const saved = gamepads[i];
|
|
7634
|
+
if (!saved || saved.id !== current[i].id || saved.connected !== current[i].connected) {
|
|
7635
|
+
hasChanged = true;
|
|
7636
|
+
break;
|
|
7637
|
+
}
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
if (hasChanged) {
|
|
7641
|
+
const prevCount = prevCountRef.current;
|
|
7642
|
+
const currentCount = current.length;
|
|
7643
|
+
if (currentCount > prevCount && prevCount >= 0 && onConnectRef.current) {
|
|
7644
|
+
const newGamepad = current[current.length - 1];
|
|
7645
|
+
onConnectRef.current(newGamepad);
|
|
7646
|
+
} else if (currentCount < prevCount && prevCount > 0 && onDisconnectRef.current) {
|
|
7647
|
+
onDisconnectRef.current();
|
|
7648
|
+
}
|
|
7649
|
+
prevCountRef.current = currentCount;
|
|
7650
|
+
setGamepads(current);
|
|
7651
|
+
}
|
|
7652
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7653
|
+
};
|
|
7654
|
+
const handleConnect = (e) => {
|
|
7655
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad connected:", e.gamepad.id);
|
|
7656
|
+
const current = getGamepads();
|
|
7657
|
+
const prevCount = prevCountRef.current;
|
|
7658
|
+
prevCountRef.current = current.length;
|
|
7659
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7660
|
+
setGamepads(current);
|
|
7661
|
+
if (onConnectRef.current && current.length > prevCount) {
|
|
7662
|
+
const newGamepad = current[current.length - 1];
|
|
7663
|
+
onConnectRef.current(newGamepad);
|
|
7664
|
+
}
|
|
7665
|
+
};
|
|
7666
|
+
const handleDisconnect = (e) => {
|
|
7667
|
+
console.log("[useGamepad] \u{1F3AE} Gamepad disconnected:", e.gamepad.id);
|
|
7668
|
+
const current = getGamepads();
|
|
7669
|
+
const prevCount = prevCountRef.current;
|
|
7670
|
+
prevCountRef.current = current.length;
|
|
7671
|
+
lastStateRef.current = current.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7672
|
+
setGamepads(current);
|
|
7673
|
+
if (onDisconnectRef.current && current.length < prevCount) {
|
|
7674
|
+
onDisconnectRef.current();
|
|
7675
|
+
}
|
|
7676
|
+
};
|
|
7677
|
+
window.addEventListener("gamepadconnected", handleConnect);
|
|
7678
|
+
window.addEventListener("gamepaddisconnected", handleDisconnect);
|
|
7679
|
+
rafRef.current = requestAnimationFrame(poll);
|
|
7680
|
+
const initial = getGamepads();
|
|
7681
|
+
if (initial.length > 0) {
|
|
7682
|
+
console.log("[useGamepad] Initial gamepads found:", initial.map((g) => g.name).join(", "));
|
|
7683
|
+
prevCountRef.current = initial.length;
|
|
7684
|
+
setGamepads(initial);
|
|
7685
|
+
lastStateRef.current = initial.map((g) => `${g.index}:${g.id}`).join("|");
|
|
7686
|
+
} else {
|
|
7687
|
+
prevCountRef.current = 0;
|
|
7688
|
+
}
|
|
7689
|
+
return () => {
|
|
7690
|
+
isActive = false;
|
|
7691
|
+
if (rafRef.current) {
|
|
7692
|
+
cancelAnimationFrame(rafRef.current);
|
|
7693
|
+
}
|
|
7694
|
+
window.removeEventListener("gamepadconnected", handleConnect);
|
|
7695
|
+
window.removeEventListener("gamepaddisconnected", handleDisconnect);
|
|
7696
|
+
};
|
|
7697
|
+
}, [getGamepads]);
|
|
7698
|
+
return {
|
|
7699
|
+
gamepads,
|
|
7700
|
+
isAnyConnected: gamepads.length > 0,
|
|
7701
|
+
connectedCount: gamepads.length,
|
|
7702
|
+
getRawGamepad,
|
|
7703
|
+
refresh
|
|
7704
|
+
};
|
|
7705
|
+
}
|
|
7706
|
+
var STANDARD_AXIS_MAP = {
|
|
7707
|
+
leftStickX: 0,
|
|
7708
|
+
leftStickY: 1,
|
|
7709
|
+
rightStickX: 2,
|
|
7710
|
+
rightStickY: 3
|
|
7711
|
+
};
|
|
7647
7712
|
function useVolume({
|
|
7648
7713
|
setVolume: setVolumeInHook,
|
|
7649
7714
|
toggleMute: toggleMuteInHook
|
|
@@ -9531,27 +9596,15 @@ function AchievementPopup({
|
|
|
9531
9596
|
onDismiss,
|
|
9532
9597
|
autoDismissMs = 5e3
|
|
9533
9598
|
}) {
|
|
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
|
-
};
|
|
9599
|
+
const { slideInRightClasses, triggerExit } = useAnimatedVisibility({
|
|
9600
|
+
exitDuration: 300,
|
|
9601
|
+
onExit: onDismiss,
|
|
9602
|
+
autoDismissMs
|
|
9603
|
+
});
|
|
9551
9604
|
return /* @__PURE__ */ jsxs(
|
|
9552
9605
|
"div",
|
|
9553
9606
|
{
|
|
9554
|
-
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${
|
|
9607
|
+
className: `fixed top-4 right-4 z-[100] transition-all duration-300 ${slideInRightClasses}`,
|
|
9555
9608
|
children: [
|
|
9556
9609
|
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-yellow-500 to-orange-500 blur-lg opacity-50 animate-pulse" }),
|
|
9557
9610
|
/* @__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 +9641,7 @@ function AchievementPopup({
|
|
|
9588
9641
|
/* @__PURE__ */ jsx(
|
|
9589
9642
|
"button",
|
|
9590
9643
|
{
|
|
9591
|
-
onClick:
|
|
9644
|
+
onClick: triggerExit,
|
|
9592
9645
|
className: "flex-shrink-0 text-gray-500 hover:text-white transition-colors",
|
|
9593
9646
|
children: /* @__PURE__ */ jsx(X, { size: 18 })
|
|
9594
9647
|
}
|