minecraft-inventory 0.1.26 → 0.1.28
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/package.json
CHANGED
|
@@ -133,15 +133,26 @@ export function InventoryOverlay({
|
|
|
133
133
|
}, [jeiOnGetRecipes, jeiOnGetUsages, pushRecipeFrame])
|
|
134
134
|
|
|
135
135
|
// Fires for any click that isn't stopped by an interactive panel (inventory, hotbar, JEI, etc.)
|
|
136
|
-
const handleBackdropClick = useCallback(() => {
|
|
137
|
-
//
|
|
138
|
-
if (
|
|
139
|
-
setFocusedSlot(null)
|
|
140
|
-
return
|
|
141
|
-
}
|
|
136
|
+
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
137
|
+
// Only handle left (drop all) and right (drop one) mouse buttons
|
|
138
|
+
if (e.button !== 0 && e.button !== 2) return
|
|
142
139
|
if (heldItem) {
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
const dropAll = e.button === 0 // LMB = drop all, RMB = drop one
|
|
141
|
+
sendAction({ type: 'drop', slotIndex: -1, all: dropAll })
|
|
142
|
+
if (dropAll) {
|
|
143
|
+
setHeldItem(null)
|
|
144
|
+
} else {
|
|
145
|
+
// Right click: drop one, keep rest on cursor
|
|
146
|
+
if (heldItem.count > 1) {
|
|
147
|
+
setHeldItem({ ...heldItem, count: heldItem.count - 1 })
|
|
148
|
+
} else {
|
|
149
|
+
setHeldItem(null)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Also clear focused slot if any
|
|
153
|
+
if (focusedSlot !== null) setFocusedSlot(null)
|
|
154
|
+
} else if (focusedSlot !== null) {
|
|
155
|
+
setFocusedSlot(null)
|
|
145
156
|
} else {
|
|
146
157
|
onClose?.()
|
|
147
158
|
}
|
|
@@ -183,7 +194,8 @@ export function InventoryOverlay({
|
|
|
183
194
|
<>
|
|
184
195
|
<div
|
|
185
196
|
className={['mc-inv-overlay', className].filter(Boolean).join(' ')}
|
|
186
|
-
|
|
197
|
+
onMouseDown={handleBackdropClick}
|
|
198
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
187
199
|
style={{
|
|
188
200
|
position: 'absolute',
|
|
189
201
|
inset: 0,
|
|
@@ -217,6 +229,7 @@ export function InventoryOverlay({
|
|
|
217
229
|
{showJEI && jeiPosition === 'left' && (
|
|
218
230
|
<div
|
|
219
231
|
className="mc-inv-overlay-jei mc-inv-overlay-jei-left"
|
|
232
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
220
233
|
onClick={(e) => e.stopPropagation()}
|
|
221
234
|
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
|
|
222
235
|
>
|
|
@@ -237,12 +250,14 @@ export function InventoryOverlay({
|
|
|
237
250
|
display: 'flex',
|
|
238
251
|
justifyContent: 'center',
|
|
239
252
|
alignItems: 'center',
|
|
253
|
+
pointerEvents: 'none',
|
|
240
254
|
}}
|
|
241
255
|
>
|
|
242
|
-
<div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
|
|
256
|
+
<div className="mc-inv-overlay-content" style={{ position: 'relative', pointerEvents: 'auto' }}>
|
|
243
257
|
{/* Inventory / Recipe view — clicks clear focused slot; slots stop propagation themselves */}
|
|
244
258
|
<div
|
|
245
259
|
className="mc-inv-overlay-window"
|
|
260
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
246
261
|
onClick={(e) => {
|
|
247
262
|
e.stopPropagation()
|
|
248
263
|
// Clicking the inventory background (not a slot) clears focused slot
|
|
@@ -274,6 +289,7 @@ export function InventoryOverlay({
|
|
|
274
289
|
{showJEI && jeiPosition === 'right' && (
|
|
275
290
|
<div
|
|
276
291
|
className="mc-inv-overlay-side mc-inv-overlay-side-right"
|
|
292
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
277
293
|
onClick={(e) => e.stopPropagation()}
|
|
278
294
|
style={{ ...sidePanelBase, right: sideGapPx, pointerEvents: 'auto' }}
|
|
279
295
|
>
|
|
@@ -310,7 +326,7 @@ export function InventoryOverlay({
|
|
|
310
326
|
lineHeight: 1,
|
|
311
327
|
}}
|
|
312
328
|
>
|
|
313
|
-
INV 0.1.
|
|
329
|
+
INV 0.1.28
|
|
314
330
|
</a>
|
|
315
331
|
)}
|
|
316
332
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
import { useFocusInputShortcut } from '../../hooks/useFocusInputShortcut'
|
|
2
3
|
import { useScale } from '../../context/ScaleContext'
|
|
3
4
|
import { useTextures } from '../../context/TextureContext'
|
|
4
5
|
import { useInventoryContext } from '../../context/InventoryContext'
|
|
5
6
|
|
|
7
|
+
const ANVIL_FOCUS_KEYS = ['KeyE'] as const
|
|
8
|
+
|
|
6
9
|
interface AnvilInputProps {
|
|
7
10
|
x: number
|
|
8
11
|
y: number
|
|
@@ -21,6 +24,8 @@ export function AnvilInput({ x, y, width, height }: AnvilInputProps) {
|
|
|
21
24
|
|
|
22
25
|
const hasInputItem = windowState?.slots.some((s) => s.index === 0 && s.item !== null) ?? false
|
|
23
26
|
|
|
27
|
+
useFocusInputShortcut(inputRef, ANVIL_FOCUS_KEYS, hasInputItem)
|
|
28
|
+
|
|
24
29
|
const textFieldUrl = textures.getGuiTextureUrl(
|
|
25
30
|
hasInputItem
|
|
26
31
|
? '1.21.11/textures/gui/sprites/container/anvil/text_field.png'
|
|
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useR
|
|
|
2
2
|
import type { ItemStack, RecipeGuide, RecipeNavFrame, BlockTextureRender } from '../../types'
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
4
|
import { useInventoryContext } from '../../context/InventoryContext'
|
|
5
|
+
import { useSlashFocusInput } from '../../hooks/useFocusInputShortcut'
|
|
5
6
|
import { Slot } from '../Slot'
|
|
6
7
|
import { StarIcon } from './StarIcon'
|
|
7
8
|
import styles from './JEI.module.css'
|
|
@@ -88,6 +89,8 @@ export function JEI({
|
|
|
88
89
|
// Self-measured dimensions so the panel can be width:100% and fill its container
|
|
89
90
|
const rootRef = useRef<HTMLDivElement>(null)
|
|
90
91
|
const gridRef = useRef<HTMLDivElement>(null)
|
|
92
|
+
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
93
|
+
useSlashFocusInput(searchInputRef, true)
|
|
91
94
|
const [measuredCols, setMeasuredCols] = useState(ITEMS_PER_ROW)
|
|
92
95
|
const [measuredRows, setMeasuredRows] = useState(5)
|
|
93
96
|
|
|
@@ -250,6 +253,7 @@ export function JEI({
|
|
|
250
253
|
}}
|
|
251
254
|
>
|
|
252
255
|
<input
|
|
256
|
+
ref={searchInputRef}
|
|
253
257
|
type="text"
|
|
254
258
|
value={search}
|
|
255
259
|
onChange={handleSearchChange}
|
|
@@ -755,13 +755,39 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
|
|
|
755
755
|
}
|
|
756
756
|
logActionEvent('connector.action.success', action)
|
|
757
757
|
} else if (action.type === 'drop') {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
758
|
+
if (action.slotIndex === -1) {
|
|
759
|
+
// Drop cursor item by clicking outside the window: slot=-999, mode=0
|
|
760
|
+
// Left click (all=true) drops entire stack, right click (all=false) drops one
|
|
761
|
+
if (!ext._client) {
|
|
762
|
+
logActionEvent('connector.action.skipped', action, { reason: 'missing_client' })
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
const windowId = bot.currentWindow ? bot.currentWindow.id : 0
|
|
766
|
+
const mouseButton = action.all ? 0 : 1
|
|
767
|
+
const packet = {
|
|
768
|
+
windowId,
|
|
769
|
+
stateId: dragStateId,
|
|
770
|
+
slot: -999,
|
|
771
|
+
mouseButton,
|
|
772
|
+
mode: 0,
|
|
773
|
+
changedSlots: [],
|
|
774
|
+
cursorItem: { present: false } as any,
|
|
775
|
+
}
|
|
776
|
+
logPacketWrite('window_click', packet)
|
|
777
|
+
ext._client.write('window_click', packet)
|
|
778
|
+
dragStateId++
|
|
779
|
+
// Skip onSetSlot() — mineflayer's selectedItem is not updated yet.
|
|
780
|
+
// The server will send set_slot/window_items to sync the cursor state.
|
|
781
|
+
} else {
|
|
782
|
+
// Drop from specific slot via Q key: mode=4
|
|
783
|
+
logHelperIntent(action, 'bot.clickWindow', {
|
|
784
|
+
slot: action.slotIndex,
|
|
785
|
+
mouseButton: action.all ? 1 : 0,
|
|
786
|
+
mode: 4,
|
|
787
|
+
})
|
|
788
|
+
await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
|
|
789
|
+
onSetSlot()
|
|
790
|
+
}
|
|
765
791
|
logActionEvent('connector.action.success', action)
|
|
766
792
|
} else if (action.type === 'close') {
|
|
767
793
|
if (win) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useMemo, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
function isTypingTarget(el: EventTarget | null): boolean {
|
|
4
|
+
if (!el || !(el instanceof HTMLElement)) return false
|
|
5
|
+
const tag = el.tagName
|
|
6
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
|
|
7
|
+
if (el.isContentEditable) return true
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SLASH_KEYS = ['Slash', 'NumpadDivide'] as const
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Focus the input when one of the given keys is pressed; prevents the key from being typed.
|
|
15
|
+
* Skips when focus is in another input/textarea/select/contenteditable (same idea as useCloseOnWClick).
|
|
16
|
+
*/
|
|
17
|
+
export function useFocusInputShortcut(
|
|
18
|
+
inputRef: RefObject<HTMLInputElement | null>,
|
|
19
|
+
keyCodes: readonly string[],
|
|
20
|
+
enabled = true,
|
|
21
|
+
) {
|
|
22
|
+
const codesSet = useMemo(() => new Set(keyCodes), [keyCodes])
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!enabled) return
|
|
26
|
+
|
|
27
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
28
|
+
if (!codesSet.has(e.code)) return
|
|
29
|
+
if (e.repeat) return
|
|
30
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return
|
|
31
|
+
if (isTypingTarget(e.target)) return
|
|
32
|
+
const el = inputRef.current
|
|
33
|
+
if (!el || el.disabled) return
|
|
34
|
+
e.preventDefault()
|
|
35
|
+
el.focus()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
window.addEventListener('keydown', onKeyDown)
|
|
39
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
40
|
+
}, [enabled, inputRef, codesSet])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `/` or numpad `/` — focus JEI search (when JEI is mounted). */
|
|
44
|
+
export function useSlashFocusInput(inputRef: RefObject<HTMLInputElement | null>, enabled = true) {
|
|
45
|
+
useFocusInputShortcut(inputRef, SLASH_KEYS, enabled)
|
|
46
|
+
}
|