poi-plugin-mcp 0.2.16 → 0.2.22

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/lib/poi-input.js CHANGED
@@ -1,302 +1,374 @@
1
- const CANONICAL_WIDTH = 1200
2
- const CANONICAL_HEIGHT = 720
3
- const DEFAULT_CLICK_DELAY_MS = 10
4
- const DRAG_MOVE_STEPS = 6
5
- const MAX_TEXT_LENGTH = 256
6
-
7
- const SUPPORTED_BUTTONS = new Set(['left', 'middle', 'right'])
8
- const SUPPORTED_KEY_EVENTS = new Set(['keyDown', 'keyUp'])
9
- const SUPPORTED_KEYS = new Set([
10
- 'Backspace',
11
- 'Delete',
12
- 'End',
13
- 'Enter',
14
- 'Escape',
15
- 'Home',
16
- 'PageDown',
17
- 'PageUp',
18
- 'Space',
19
- 'Tab',
20
- 'ArrowDown',
21
- 'ArrowLeft',
22
- 'ArrowRight',
23
- 'ArrowUp',
24
- ])
25
-
26
- function createPoiInputProvider(options = {}) {
27
- const getStore = options.getStore || defaultGetStore
28
- const resolveWebContents =
29
- options.resolveWebContents || defaultResolveWebContents
30
- const delay = options.delay || defaultDelay
31
- const clickDelayMs = options.clickDelayMs == null
32
- ? DEFAULT_CLICK_DELAY_MS
33
- : options.clickDelayMs
34
-
35
- if (
36
- !Number.isInteger(clickDelayMs) ||
37
- clickDelayMs < 1 ||
38
- clickDelayMs > 100
39
- ) {
40
- throw new Error('clickDelayMs must be an integer from 1 to 100')
41
- }
42
-
43
- return async function performPoiInput(operation) {
44
- validateOperationObject(operation)
45
- const layout = readLayout(getStore, resolveWebContents)
46
-
47
- switch (operation.operation) {
48
- case 'click':
49
- validateClick(operation)
50
- await sendClick(layout, operation, delay, clickDelayMs)
51
- return 'click'
52
- case 'drag':
53
- validateDrag(operation)
54
- await sendDrag(layout, operation, delay)
55
- return 'drag'
56
- case 'key':
57
- validateKey(operation)
58
- await layout.webContents.sendInputEvent({
59
- type: operation.event,
60
- keyCode: operation.key,
61
- })
62
- return 'key'
63
- case 'text':
64
- validateText(operation)
65
- for (const character of operation.text) {
66
- await layout.webContents.sendInputEvent({
67
- type: 'char',
68
- keyCode: character,
69
- })
70
- }
71
- return 'text'
72
- default:
73
- throw new Error(`Unsupported input operation: ${String(operation.operation)}`)
74
- }
75
- }
76
- }
77
-
78
- function validateOperationObject(operation) {
79
- if (
80
- !operation ||
81
- typeof operation !== 'object' ||
82
- Array.isArray(operation)
83
- ) {
84
- throw new Error('Input must be one operation object')
85
- }
86
- }
87
-
88
- function readLayout(getStore, resolveWebContents) {
89
- const layout = getStore('layout.webview')
90
- if (!layout || !layout.ref) {
91
- throw new Error('Poi game WebView is not ready')
92
- }
93
- if (
94
- !Number.isFinite(layout.width) ||
95
- layout.width <= 0 ||
96
- !Number.isFinite(layout.height) ||
97
- layout.height <= 0
98
- ) {
99
- throw new Error('Poi game WebView dimensions must be positive finite numbers')
100
- }
101
- let webContents
102
- if (typeof layout.ref.getWebContents === 'function') {
103
- webContents = layout.ref.getWebContents()
104
- } else if (typeof layout.ref.getWebContentsId === 'function') {
105
- const webContentsId = layout.ref.getWebContentsId()
106
- if (!Number.isInteger(webContentsId) || webContentsId <= 0) {
107
- throw new Error('Poi game WebContents id is invalid')
108
- }
109
- webContents = resolveWebContents(webContentsId)
110
- } else {
111
- throw new Error('Poi game WebView is not ready')
112
- }
113
- if (!webContents || typeof webContents.sendInputEvent !== 'function') {
114
- throw new Error('Poi game WebContents is not ready')
115
- }
116
- return { ...layout, webContents }
117
- }
118
-
119
- function validateClick(operation) {
120
- assertExactFields(operation, ['operation', 'x', 'y', 'button'])
121
- if (!Number.isFinite(operation.x) || !Number.isFinite(operation.y)) {
122
- throw new Error('Click coordinates must be finite numbers')
123
- }
124
- if (
125
- operation.x < 0 ||
126
- operation.x >= CANONICAL_WIDTH ||
127
- operation.y < 0 ||
128
- operation.y >= CANONICAL_HEIGHT
129
- ) {
130
- throw new Error('Click coordinates must be within canonical bounds')
131
- }
132
- if (!SUPPORTED_BUTTONS.has(operation.button)) {
133
- throw new Error(`Unsupported mouse button: ${String(operation.button)}`)
134
- }
135
- }
136
-
137
- function validateDrag(operation) {
138
- assertExactFields(operation, [
139
- 'operation',
140
- 'fromX',
141
- 'fromY',
142
- 'toX',
143
- 'toY',
144
- 'durationMs',
145
- 'button',
146
- ])
147
- const coordinates = [
148
- operation.fromX,
149
- operation.fromY,
150
- operation.toX,
151
- operation.toY,
152
- ]
153
- if (coordinates.some((coordinate) => !Number.isFinite(coordinate))) {
154
- throw new Error('Drag coordinates must be finite numbers')
155
- }
156
- if (
157
- operation.fromX < 0 ||
158
- operation.fromX >= CANONICAL_WIDTH ||
159
- operation.toX < 0 ||
160
- operation.toX >= CANONICAL_WIDTH ||
161
- operation.fromY < 0 ||
162
- operation.fromY >= CANONICAL_HEIGHT ||
163
- operation.toY < 0 ||
164
- operation.toY >= CANONICAL_HEIGHT
165
- ) {
166
- throw new Error('Drag coordinates must be within canonical bounds')
167
- }
168
- if (!Number.isInteger(operation.durationMs)) {
169
- throw new Error('Drag durationMs must be an integer')
170
- }
171
- if (operation.durationMs < 50 || operation.durationMs > 2000) {
172
- throw new Error('Drag durationMs must be from 50 to 2000')
173
- }
174
- if (!SUPPORTED_BUTTONS.has(operation.button)) {
175
- throw new Error(`Unsupported mouse button: ${String(operation.button)}`)
176
- }
177
- }
178
-
179
- function validateKey(operation) {
180
- assertExactFields(operation, ['operation', 'event', 'key'])
181
- if (!SUPPORTED_KEY_EVENTS.has(operation.event)) {
182
- throw new Error(`Unsupported key event: ${String(operation.event)}`)
183
- }
184
- if (!SUPPORTED_KEYS.has(operation.key)) {
185
- throw new Error(`Unsupported key: ${String(operation.key)}`)
186
- }
187
- }
188
-
189
- function validateText(operation) {
190
- assertExactFields(operation, ['operation', 'text'])
191
- if (
192
- typeof operation.text !== 'string' ||
193
- operation.text.length === 0 ||
194
- operation.text.length > MAX_TEXT_LENGTH
195
- ) {
196
- throw new Error('Literal text must contain 1 to 256 characters')
197
- }
198
- if (/[\u0000-\u001f\u007f-\u009f]/u.test(operation.text)) {
199
- throw new Error('Literal text must contain printable characters only')
200
- }
201
- }
202
-
203
- function assertExactFields(operation, allowedFields) {
204
- const allowed = new Set(allowedFields)
205
- const unexpected = Object.keys(operation).find((field) => !allowed.has(field))
206
- if (unexpected) {
207
- throw new Error(`Unexpected field for ${operation.operation}: ${unexpected}`)
208
- }
209
- const missing = allowedFields.find((field) => !Object.hasOwn(operation, field))
210
- if (missing) {
211
- throw new Error(`Missing field for ${operation.operation}: ${missing}`)
212
- }
213
- }
214
-
215
- async function sendClick(layout, operation, delay, clickDelayMs) {
216
- const event = {
217
- x: Math.floor((operation.x * layout.width) / CANONICAL_WIDTH),
218
- y: Math.floor((operation.y * layout.height) / CANONICAL_HEIGHT),
219
- button: operation.button,
220
- clickCount: 1,
221
- }
222
-
223
- await layout.webContents.sendInputEvent({ type: 'mouseDown', ...event })
224
- try {
225
- await delay(clickDelayMs)
226
- } finally {
227
- await layout.webContents.sendInputEvent({ type: 'mouseUp', ...event })
228
- }
229
- }
230
-
231
- async function sendDrag(layout, operation, delay) {
232
- const start = {
233
- x: Math.floor((operation.fromX * layout.width) / CANONICAL_WIDTH),
234
- y: Math.floor((operation.fromY * layout.height) / CANONICAL_HEIGHT),
235
- }
236
- const destination = {
237
- x: Math.floor((operation.toX * layout.width) / CANONICAL_WIDTH),
238
- y: Math.floor((operation.toY * layout.height) / CANONICAL_HEIGHT),
239
- }
240
- const mouseButton = { button: operation.button }
241
-
242
- await layout.webContents.sendInputEvent({
243
- type: 'mouseMove',
244
- ...start,
245
- ...mouseButton,
246
- })
247
- try {
248
- await layout.webContents.sendInputEvent({
249
- type: 'mouseDown',
250
- ...start,
251
- ...mouseButton,
252
- clickCount: 1,
253
- })
254
- for (let step = 1; step <= DRAG_MOVE_STEPS; step += 1) {
255
- const elapsed = Math.round((operation.durationMs * step) / DRAG_MOVE_STEPS)
256
- const previousElapsed = Math.round(
257
- (operation.durationMs * (step - 1)) / DRAG_MOVE_STEPS,
258
- )
259
- await delay(elapsed - previousElapsed)
260
- await layout.webContents.sendInputEvent({
261
- type: 'mouseMove',
262
- x: Math.round(
263
- start.x + ((destination.x - start.x) * step) / DRAG_MOVE_STEPS,
264
- ),
265
- y: Math.round(
266
- start.y + ((destination.y - start.y) * step) / DRAG_MOVE_STEPS,
267
- ),
268
- ...mouseButton,
269
- })
270
- }
271
- } finally {
272
- await layout.webContents.sendInputEvent({
273
- type: 'mouseUp',
274
- ...destination,
275
- ...mouseButton,
276
- clickCount: 1,
277
- })
278
- }
279
- }
280
-
281
- function defaultDelay(milliseconds) {
282
- return new Promise((resolve) => setTimeout(resolve, milliseconds))
283
- }
284
-
285
- function defaultGetStore(path) {
286
- if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
287
- return window.getStore(path)
288
- }
289
- return null
290
- }
291
-
292
- function defaultResolveWebContents(webContentsId) {
293
- const { webContents } = require('@electron/remote')
294
- return webContents.fromId(webContentsId)
295
- }
296
-
297
- module.exports = {
298
- CANONICAL_HEIGHT,
299
- CANONICAL_WIDTH,
300
- MAX_TEXT_LENGTH,
301
- createPoiInputProvider,
302
- }
1
+ const CANONICAL_WIDTH = 1200
2
+ const CANONICAL_HEIGHT = 720
3
+ const DRAG_MOVE_STEPS = 6
4
+ const DRAG_RELEASE_DELAY_MS = 120
5
+ const MAX_TEXT_LENGTH = 256
6
+
7
+ const SUPPORTED_BUTTONS = new Set(['left', 'middle', 'right'])
8
+ const SUPPORTED_KEY_EVENTS = new Set(['keyDown', 'keyUp'])
9
+ const SUPPORTED_KEYS = new Set([
10
+ 'Backspace',
11
+ 'Delete',
12
+ 'End',
13
+ 'Enter',
14
+ 'Escape',
15
+ 'Home',
16
+ 'PageDown',
17
+ 'PageUp',
18
+ 'Space',
19
+ 'Tab',
20
+ 'ArrowDown',
21
+ 'ArrowLeft',
22
+ 'ArrowRight',
23
+ 'ArrowUp',
24
+ ])
25
+
26
+ function createPoiInputProvider(options = {}) {
27
+ const getStore = options.getStore || defaultGetStore
28
+ const resolveWebContents =
29
+ options.resolveWebContents || defaultResolveWebContents
30
+ const delay = options.delay || defaultDelay
31
+
32
+ return async function performPoiInput(operation) {
33
+ validateOperationObject(operation)
34
+ const layout = readLayout(getStore, resolveWebContents)
35
+ // Live 2026-08-24: synthetic clicks reach the game and take effect in a
36
+ // visible, non-focused window; they only stall when the window is
37
+ // minimized/occluded and Chromium throttles the renderer (PIXI ticker
38
+ // frozen). So: un-throttle the webContents, restore a minimized window,
39
+ // and keep the document focused at the webContents level — never steal
40
+ // the user's OS foreground (the Windows foreground lock silently ignored
41
+ // the old win.focus() anyway).
42
+ prepareGameWindowForInput(layout.webContents)
43
+
44
+ switch (operation.operation) {
45
+ case 'move':
46
+ validateMove(operation)
47
+ sendMove(layout, operation)
48
+ return 'move'
49
+ case 'click':
50
+ validateClick(operation)
51
+ sendClick(layout, operation)
52
+ return 'click'
53
+ case 'drag':
54
+ validateDrag(operation)
55
+ await sendDrag(layout, operation, delay)
56
+ return 'drag'
57
+ case 'key':
58
+ validateKey(operation)
59
+ await layout.webContents.sendInputEvent({
60
+ type: operation.event,
61
+ keyCode: operation.key,
62
+ })
63
+ return 'key'
64
+ case 'text':
65
+ validateText(operation)
66
+ for (const character of operation.text) {
67
+ await layout.webContents.sendInputEvent({
68
+ type: 'char',
69
+ keyCode: character,
70
+ })
71
+ }
72
+ return 'text'
73
+ default:
74
+ throw new Error(`Unsupported input operation: ${String(operation.operation)}`)
75
+ }
76
+ }
77
+ }
78
+
79
+ function validateOperationObject(operation) {
80
+ if (
81
+ !operation ||
82
+ typeof operation !== 'object' ||
83
+ Array.isArray(operation)
84
+ ) {
85
+ throw new Error('Input must be one operation object')
86
+ }
87
+ }
88
+
89
+ function prepareGameWindowForInput(webContents) {
90
+ try {
91
+ if (typeof webContents.setBackgroundThrottling === 'function') {
92
+ webContents.setBackgroundThrottling(false)
93
+ }
94
+ } catch (_throttleError) {
95
+ // Throttling control is best-effort; input below is still delivered.
96
+ }
97
+ try {
98
+ const { BrowserWindow } = require('electron')
99
+ const win = BrowserWindow.fromWebContents(
100
+ webContents.hostWebContents || webContents,
101
+ )
102
+ if (win && win.isMinimized()) {
103
+ // Restoring resumes the throttled renderer; it does not need the
104
+ // OS foreground for synthetic input to work (verified 2026-08-24).
105
+ win.restore()
106
+ }
107
+ } catch (_windowError) {
108
+ // Fall through to webContents-level focus below.
109
+ }
110
+ try {
111
+ if (typeof webContents.focus === 'function') webContents.focus()
112
+ } catch (_focusError) {
113
+ // Input below is still delivered and may work without focus.
114
+ }
115
+ }
116
+
117
+ function readLayout(getStore, resolveWebContents) {
118
+ const layout = getStore('layout.webview')
119
+ if (!layout || !layout.ref) {
120
+ throw new Error('Poi game WebView is not ready')
121
+ }
122
+ if (
123
+ !Number.isFinite(layout.width) ||
124
+ layout.width <= 0 ||
125
+ !Number.isFinite(layout.height) ||
126
+ layout.height <= 0
127
+ ) {
128
+ throw new Error('Poi game WebView dimensions must be positive finite numbers')
129
+ }
130
+ let webContents
131
+ if (typeof layout.ref.getWebContents === 'function') {
132
+ webContents = layout.ref.getWebContents()
133
+ } else if (typeof layout.ref.getWebContentsId === 'function') {
134
+ const webContentsId = layout.ref.getWebContentsId()
135
+ if (!Number.isInteger(webContentsId) || webContentsId <= 0) {
136
+ throw new Error('Poi game WebContents id is invalid')
137
+ }
138
+ webContents = resolveWebContents(webContentsId)
139
+ } else {
140
+ throw new Error('Poi game WebView is not ready')
141
+ }
142
+ if (!webContents || typeof webContents.sendInputEvent !== 'function') {
143
+ throw new Error('Poi game WebContents is not ready')
144
+ }
145
+ return { ...layout, webContents }
146
+ }
147
+
148
+ function validateClick(operation) {
149
+ assertExactFields(operation, ['operation', 'x', 'y', 'button'])
150
+ if (!Number.isFinite(operation.x) || !Number.isFinite(operation.y)) {
151
+ throw new Error('Click coordinates must be finite numbers')
152
+ }
153
+ if (
154
+ operation.x < 0 ||
155
+ operation.x >= CANONICAL_WIDTH ||
156
+ operation.y < 0 ||
157
+ operation.y >= CANONICAL_HEIGHT
158
+ ) {
159
+ throw new Error('Click coordinates must be within canonical bounds')
160
+ }
161
+ if (!SUPPORTED_BUTTONS.has(operation.button)) {
162
+ throw new Error(`Unsupported mouse button: ${String(operation.button)}`)
163
+ }
164
+ }
165
+
166
+ function validateMove(operation) {
167
+ assertExactFields(operation, ['operation', 'x', 'y'])
168
+ if (!Number.isFinite(operation.x) || !Number.isFinite(operation.y)) {
169
+ throw new Error('Move coordinates must be finite numbers')
170
+ }
171
+ if (
172
+ operation.x < 0 ||
173
+ operation.x >= CANONICAL_WIDTH ||
174
+ operation.y < 0 ||
175
+ operation.y >= CANONICAL_HEIGHT
176
+ ) {
177
+ throw new Error('Move coordinates must be within canonical bounds')
178
+ }
179
+ }
180
+
181
+ function validateDrag(operation) {
182
+ assertExactFields(operation, [
183
+ 'operation',
184
+ 'fromX',
185
+ 'fromY',
186
+ 'toX',
187
+ 'toY',
188
+ 'durationMs',
189
+ 'button',
190
+ ])
191
+ const coordinates = [
192
+ operation.fromX,
193
+ operation.fromY,
194
+ operation.toX,
195
+ operation.toY,
196
+ ]
197
+ if (coordinates.some((coordinate) => !Number.isFinite(coordinate))) {
198
+ throw new Error('Drag coordinates must be finite numbers')
199
+ }
200
+ if (
201
+ operation.fromX < 0 ||
202
+ operation.fromX >= CANONICAL_WIDTH ||
203
+ operation.toX < 0 ||
204
+ operation.toX >= CANONICAL_WIDTH ||
205
+ operation.fromY < 0 ||
206
+ operation.fromY >= CANONICAL_HEIGHT ||
207
+ operation.toY < 0 ||
208
+ operation.toY >= CANONICAL_HEIGHT
209
+ ) {
210
+ throw new Error('Drag coordinates must be within canonical bounds')
211
+ }
212
+ if (!Number.isInteger(operation.durationMs)) {
213
+ throw new Error('Drag durationMs must be an integer')
214
+ }
215
+ if (operation.durationMs < 50 || operation.durationMs > 2000) {
216
+ throw new Error('Drag durationMs must be from 50 to 2000')
217
+ }
218
+ if (!SUPPORTED_BUTTONS.has(operation.button)) {
219
+ throw new Error(`Unsupported mouse button: ${String(operation.button)}`)
220
+ }
221
+ }
222
+
223
+ function validateKey(operation) {
224
+ assertExactFields(operation, ['operation', 'event', 'key'])
225
+ if (!SUPPORTED_KEY_EVENTS.has(operation.event)) {
226
+ throw new Error(`Unsupported key event: ${String(operation.event)}`)
227
+ }
228
+ if (!SUPPORTED_KEYS.has(operation.key)) {
229
+ throw new Error(`Unsupported key: ${String(operation.key)}`)
230
+ }
231
+ }
232
+
233
+ function validateText(operation) {
234
+ assertExactFields(operation, ['operation', 'text'])
235
+ if (
236
+ typeof operation.text !== 'string' ||
237
+ operation.text.length === 0 ||
238
+ operation.text.length > MAX_TEXT_LENGTH
239
+ ) {
240
+ throw new Error('Literal text must contain 1 to 256 characters')
241
+ }
242
+ if (/[\u0000-\u001f\u007f-\u009f]/u.test(operation.text)) {
243
+ throw new Error('Literal text must contain printable characters only')
244
+ }
245
+ }
246
+
247
+ function assertExactFields(operation, allowedFields) {
248
+ const allowed = new Set(allowedFields)
249
+ const unexpected = Object.keys(operation).find((field) => !allowed.has(field))
250
+ if (unexpected) {
251
+ throw new Error(`Unexpected field for ${operation.operation}: ${unexpected}`)
252
+ }
253
+ const missing = allowedFields.find((field) => !Object.hasOwn(operation, field))
254
+ if (missing) {
255
+ throw new Error(`Missing field for ${operation.operation}: ${missing}`)
256
+ }
257
+ }
258
+
259
+ function sendMove(layout, operation) {
260
+ sendPointerEnterAndMove(layout.webContents, {
261
+ x: Math.floor((operation.x * layout.width) / CANONICAL_WIDTH),
262
+ y: Math.floor((operation.y * layout.height) / CANONICAL_HEIGHT),
263
+ })
264
+ }
265
+
266
+ function sendClick(layout, operation) {
267
+ const point = {
268
+ x: Math.floor((operation.x * layout.width) / CANONICAL_WIDTH),
269
+ y: Math.floor((operation.y * layout.height) / CANONICAL_HEIGHT),
270
+ button: operation.button,
271
+ }
272
+
273
+ // Match poi-plugin-kancolle-assistant's long-running browser click sequence.
274
+ layout.webContents.sendInputEvent({ type: 'mouseMove', ...point })
275
+ layout.webContents.sendInputEvent({
276
+ type: 'mouseDown',
277
+ ...point,
278
+ clickCount: 3,
279
+ })
280
+ layout.webContents.sendInputEvent({ type: 'mouseUp', ...point, clickCount: 3 })
281
+ layout.webContents.sendInputEvent({
282
+ type: 'mouseMove',
283
+ x: 0,
284
+ y: 0,
285
+ button: operation.button,
286
+ })
287
+ }
288
+
289
+ async function sendDrag(layout, operation, delay) {
290
+ const start = {
291
+ x: Math.floor((operation.fromX * layout.width) / CANONICAL_WIDTH),
292
+ y: Math.floor((operation.fromY * layout.height) / CANONICAL_HEIGHT),
293
+ }
294
+ const destination = {
295
+ x: Math.floor((operation.toX * layout.width) / CANONICAL_WIDTH),
296
+ y: Math.floor((operation.toY * layout.height) / CANONICAL_HEIGHT),
297
+ }
298
+ const mouseButton = { button: operation.button }
299
+
300
+ sendPointerEnterAndMove(layout.webContents, start)
301
+ try {
302
+ await layout.webContents.sendInputEvent({
303
+ type: 'mouseDown',
304
+ ...start,
305
+ ...mouseButton,
306
+ clickCount: 1,
307
+ })
308
+ let previousX = start.x
309
+ let previousY = start.y
310
+ for (let step = 1; step <= DRAG_MOVE_STEPS; step += 1) {
311
+ const elapsed = Math.round((operation.durationMs * step) / DRAG_MOVE_STEPS)
312
+ const previousElapsed = Math.round(
313
+ (operation.durationMs * (step - 1)) / DRAG_MOVE_STEPS,
314
+ )
315
+ await delay(elapsed - previousElapsed)
316
+ const x = Math.round(
317
+ start.x + ((destination.x - start.x) * step) / DRAG_MOVE_STEPS,
318
+ )
319
+ const y = Math.round(
320
+ start.y + ((destination.y - start.y) * step) / DRAG_MOVE_STEPS,
321
+ )
322
+ await layout.webContents.sendInputEvent({
323
+ type: 'mouseMove',
324
+ x,
325
+ y,
326
+ movementX: x - previousX,
327
+ movementY: y - previousY,
328
+ ...mouseButton,
329
+ })
330
+ previousX = x
331
+ previousY = y
332
+ }
333
+ // The game's drag targets (e.g. the organization fleet tab combine drag)
334
+ // require a short hold at the destination before the release; releasing
335
+ // in the same tick as the last move drops the gesture (verified against
336
+ // poi-plugin-kancolle-assistant's mouseMove drag implementation).
337
+ await delay(DRAG_RELEASE_DELAY_MS)
338
+ } finally {
339
+ await layout.webContents.sendInputEvent({
340
+ type: 'mouseUp',
341
+ ...destination,
342
+ ...mouseButton,
343
+ clickCount: 1,
344
+ })
345
+ }
346
+ }
347
+
348
+ function sendPointerEnterAndMove(webContents, point) {
349
+ webContents.sendInputEvent({ type: 'mouseEnter', x: point.x, y: point.y })
350
+ webContents.sendInputEvent({ type: 'mouseMove', x: point.x, y: point.y })
351
+ }
352
+
353
+ function defaultDelay(milliseconds) {
354
+ return new Promise((resolve) => setTimeout(resolve, milliseconds))
355
+ }
356
+
357
+ function defaultGetStore(path) {
358
+ if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
359
+ return window.getStore(path)
360
+ }
361
+ return null
362
+ }
363
+
364
+ function defaultResolveWebContents(webContentsId) {
365
+ const { webContents } = require('@electron/remote')
366
+ return webContents.fromId(webContentsId)
367
+ }
368
+
369
+ module.exports = {
370
+ CANONICAL_HEIGHT,
371
+ CANONICAL_WIDTH,
372
+ MAX_TEXT_LENGTH,
373
+ createPoiInputProvider,
374
+ }