poi-plugin-reload-game-button 0.3.0

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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # poi-plugin-reload-game-button
2
+
3
+ ## 中文
4
+
5
+ 适用于 poi 的触摸友好悬浮按钮插件,用来只重新载入游戏本体。
6
+
7
+ 这个按钮调用 poi 现有的 `gameReload()` 逻辑。如果该逻辑无法加载,会回退到与 poi 相同的
8
+ webview JavaScript 重新载入方式。
9
+
10
+ 悬浮窗默认显示在窗口右侧。拖动顶部标题栏可以移动,拖动右下角可以调整大小,位置和尺寸会
11
+ 保存到 `localStorage`。
12
+
13
+ 点击悬浮窗右上角关闭按钮可以隐藏它。隐藏后,在 poi 的插件主页打开本插件,点击“显示悬浮窗”
14
+ 即可重新打开。
15
+
16
+ 仓库地址:https://github.com/DuskWhite/poi-plugin-reload-game-button
17
+
18
+ ## English
19
+
20
+ A touch-friendly floating panel plugin for poi that reloads only the game frame.
21
+
22
+ The button calls poi's existing `gameReload()` helper. If that helper cannot be loaded, it falls back
23
+ to the same webview JavaScript used by poi.
24
+
25
+ The panel starts on the right side of the window. Drag the top handle to move it, and drag the
26
+ bottom-right corner to resize it. Position and size are saved in `localStorage`.
27
+
28
+ Use the close button in the panel header to hide it. Open the plugin page from poi's plugin list and
29
+ click "Show floating panel" to bring it back.
30
+
31
+ Repository: https://github.com/DuskWhite/poi-plugin-reload-game-button
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "Reload Game Button",
3
+ "description": "A touch-friendly floating button for reloading only the game frame.",
4
+ "showPanel": "Show floating panel",
5
+ "reloadGame": "Reload game",
6
+ "repository": "Repository",
7
+ "panelVisible": "Floating panel is visible.",
8
+ "panelHidden": "Floating panel is hidden.",
9
+ "close": "Close"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "重新载入游戏",
3
+ "description": "适合触摸屏使用的悬浮按钮,只重新载入游戏本体。",
4
+ "showPanel": "显示悬浮窗",
5
+ "reloadGame": "重新载入游戏",
6
+ "repository": "仓库地址",
7
+ "panelVisible": "悬浮窗已显示。",
8
+ "panelHidden": "悬浮窗已隐藏。",
9
+ "close": "关闭"
10
+ }
package/index.js ADDED
@@ -0,0 +1,454 @@
1
+ 'use strict'
2
+
3
+ const PLUGIN_ID = 'poi-plugin-reload-game-button'
4
+ const PANEL_ID = 'poi-reload-game-panel'
5
+ const STYLE_ID = 'poi-reload-game-button-style'
6
+ const STORAGE_KEY = 'poi-plugin-reload-game-button:panel-state'
7
+ const CLOSED_STORAGE_KEY = 'poi-plugin-reload-game-button:panel-closed'
8
+ const SHOW_PANEL_EVENT = 'poi-plugin-reload-game-button:show-panel'
9
+ const RELOAD_GAME_EVENT = 'poi-plugin-reload-game-button:reload-game'
10
+ const RELOAD_TITLE = '重新载入游戏'
11
+
12
+ const DEFAULT_STATE = {
13
+ width: 168,
14
+ height: 78,
15
+ top: null,
16
+ left: null,
17
+ }
18
+
19
+ let observer = null
20
+ let retryTimer = null
21
+
22
+ function reloadGameFallback() {
23
+ const { getStore } = require('views/create-store')
24
+ getStore('layout.webview.ref')?.executeJavaScript(`
25
+ var doc;
26
+ if (document.getElementById('game_frame')) {
27
+ doc = document.getElementById('game_frame').contentDocument;
28
+ } else {
29
+ doc = document;
30
+ }
31
+
32
+ var game = doc.getElementById('htmlWrap');
33
+ if (game) {
34
+ game.contentWindow.location.reload()
35
+ }
36
+ `)
37
+ }
38
+
39
+ function reloadGame() {
40
+ try {
41
+ const { gameReload } = require('views/services/utils')
42
+ gameReload()
43
+ } catch (error) {
44
+ console.warn(`${PLUGIN_ID}: falling back to inline reload`, error)
45
+ reloadGameFallback()
46
+ }
47
+ }
48
+
49
+ function readPanelState() {
50
+ try {
51
+ const raw = window.localStorage.getItem(STORAGE_KEY)
52
+ if (!raw) {
53
+ return { ...DEFAULT_STATE }
54
+ }
55
+
56
+ const parsed = JSON.parse(raw)
57
+ return {
58
+ width: Number.isFinite(parsed.width) ? parsed.width : DEFAULT_STATE.width,
59
+ height: Number.isFinite(parsed.height) ? parsed.height : DEFAULT_STATE.height,
60
+ top: Number.isFinite(parsed.top) ? parsed.top : DEFAULT_STATE.top,
61
+ left: Number.isFinite(parsed.left) ? parsed.left : DEFAULT_STATE.left,
62
+ }
63
+ } catch (error) {
64
+ console.warn(`${PLUGIN_ID}: failed to read panel state`, error)
65
+ return { ...DEFAULT_STATE }
66
+ }
67
+ }
68
+
69
+ function writePanelState(panel) {
70
+ const rect = panel.getBoundingClientRect()
71
+ const state = {
72
+ width: Math.round(rect.width),
73
+ height: Math.round(rect.height),
74
+ top: Math.round(rect.top),
75
+ left: Math.round(rect.left),
76
+ }
77
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
78
+ }
79
+
80
+ function isPanelClosed() {
81
+ return window.localStorage.getItem(CLOSED_STORAGE_KEY) === 'true'
82
+ }
83
+
84
+ function setPanelClosed(closed) {
85
+ window.localStorage.setItem(CLOSED_STORAGE_KEY, closed ? 'true' : 'false')
86
+ }
87
+
88
+ function clamp(value, min, max) {
89
+ return Math.min(Math.max(value, min), max)
90
+ }
91
+
92
+ function clampPanel(panel) {
93
+ const rect = panel.getBoundingClientRect()
94
+ const left = clamp(rect.left, 0, Math.max(0, window.innerWidth - rect.width))
95
+ const top = clamp(rect.top, 0, Math.max(0, window.innerHeight - rect.height))
96
+ panel.style.left = `${Math.round(left)}px`
97
+ panel.style.top = `${Math.round(top)}px`
98
+ panel.style.right = 'auto'
99
+ }
100
+
101
+ function applyPanelState(panel) {
102
+ const state = readPanelState()
103
+ const width = clamp(state.width, 120, Math.max(120, window.innerWidth))
104
+ const height = clamp(state.height, 58, Math.max(58, window.innerHeight))
105
+
106
+ panel.style.width = `${Math.round(width)}px`
107
+ panel.style.height = `${Math.round(height)}px`
108
+
109
+ if (state.left == null || state.top == null) {
110
+ panel.style.right = '14px'
111
+ panel.style.left = 'auto'
112
+ panel.style.top = `${Math.round((window.innerHeight - height) / 2)}px`
113
+ } else {
114
+ panel.style.left = `${Math.round(state.left)}px`
115
+ panel.style.top = `${Math.round(state.top)}px`
116
+ panel.style.right = 'auto'
117
+ }
118
+
119
+ window.requestAnimationFrame(() => clampPanel(panel))
120
+ }
121
+
122
+ function ensureStyle() {
123
+ if (document.getElementById(STYLE_ID)) {
124
+ return
125
+ }
126
+
127
+ const style = document.createElement('style')
128
+ style.id = STYLE_ID
129
+ style.textContent = `
130
+ #${PANEL_ID} {
131
+ backdrop-filter: blur(12px);
132
+ background: rgba(35, 40, 48, 0.88);
133
+ border: 1px solid rgba(255, 255, 255, 0.18);
134
+ border-radius: 8px;
135
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
136
+ box-sizing: border-box;
137
+ color: #fff;
138
+ display: flex;
139
+ flex-direction: column;
140
+ min-height: 58px;
141
+ min-width: 120px;
142
+ overflow: hidden;
143
+ position: fixed;
144
+ z-index: 2147483000;
145
+ }
146
+
147
+ #${PANEL_ID} .poi-reload-game-panel-handle {
148
+ align-items: center;
149
+ background: rgba(255, 255, 255, 0.1);
150
+ cursor: move;
151
+ display: flex;
152
+ flex: 0 0 22px;
153
+ font-size: 11px;
154
+ font-weight: 600;
155
+ justify-content: center;
156
+ letter-spacing: 0;
157
+ line-height: 22px;
158
+ touch-action: none;
159
+ user-select: none;
160
+ }
161
+
162
+ #${PANEL_ID} .poi-reload-game-panel-title {
163
+ flex: 1 1 auto;
164
+ text-align: center;
165
+ }
166
+
167
+ #${PANEL_ID} .poi-reload-game-panel-close {
168
+ align-items: center;
169
+ background: transparent;
170
+ border: 0;
171
+ color: rgba(255, 255, 255, 0.86);
172
+ cursor: pointer;
173
+ display: flex;
174
+ flex: 0 0 26px;
175
+ font-size: 17px;
176
+ height: 22px;
177
+ justify-content: center;
178
+ line-height: 1;
179
+ padding: 0;
180
+ touch-action: manipulation;
181
+ }
182
+
183
+ #${PANEL_ID} .poi-reload-game-panel-close:active {
184
+ background: rgba(255, 255, 255, 0.12);
185
+ }
186
+
187
+ #${PANEL_ID} .poi-reload-game-panel-button {
188
+ align-items: center;
189
+ background: #d9822b;
190
+ border: 0;
191
+ color: #fff;
192
+ cursor: pointer;
193
+ display: flex;
194
+ flex: 1 1 auto;
195
+ font-size: 18px;
196
+ font-weight: 700;
197
+ justify-content: center;
198
+ letter-spacing: 0;
199
+ line-height: 1.2;
200
+ min-height: 36px;
201
+ padding: 8px 12px;
202
+ text-align: center;
203
+ touch-action: manipulation;
204
+ user-select: none;
205
+ width: 100%;
206
+ }
207
+
208
+ #${PANEL_ID} .poi-reload-game-panel-button:active {
209
+ background: #bf7326;
210
+ }
211
+
212
+ #${PANEL_ID} .poi-reload-game-panel-resizer {
213
+ border-bottom: 12px solid rgba(255, 255, 255, 0.72);
214
+ border-left: 12px solid transparent;
215
+ bottom: 4px;
216
+ cursor: nwse-resize;
217
+ height: 0;
218
+ position: absolute;
219
+ right: 4px;
220
+ touch-action: none;
221
+ width: 0;
222
+ }
223
+ `
224
+ document.head.appendChild(style)
225
+ }
226
+
227
+ function createPanel() {
228
+ const panel = document.createElement('div')
229
+ panel.id = PANEL_ID
230
+
231
+ const handle = document.createElement('div')
232
+ handle.className = 'poi-reload-game-panel-handle'
233
+
234
+ const title = document.createElement('div')
235
+ title.className = 'poi-reload-game-panel-title'
236
+ title.textContent = 'Reload'
237
+
238
+ const close = document.createElement('button')
239
+ close.type = 'button'
240
+ close.className = 'poi-reload-game-panel-close'
241
+ close.title = '关闭'
242
+ close.setAttribute('aria-label', '关闭')
243
+ close.textContent = '×'
244
+ close.addEventListener('pointerdown', (event) => {
245
+ event.stopPropagation()
246
+ })
247
+ close.addEventListener('click', (event) => {
248
+ event.preventDefault()
249
+ event.stopPropagation()
250
+ closePanel(panel)
251
+ })
252
+
253
+ handle.appendChild(title)
254
+ handle.appendChild(close)
255
+
256
+ const button = document.createElement('button')
257
+ button.type = 'button'
258
+ button.className = 'poi-reload-game-panel-button'
259
+ button.title = RELOAD_TITLE
260
+ button.setAttribute('aria-label', RELOAD_TITLE)
261
+ button.textContent = RELOAD_TITLE
262
+ button.addEventListener('click', (event) => {
263
+ event.preventDefault()
264
+ event.stopPropagation()
265
+ reloadGame()
266
+ })
267
+
268
+ const resizer = document.createElement('div')
269
+ resizer.className = 'poi-reload-game-panel-resizer'
270
+
271
+ panel.appendChild(handle)
272
+ panel.appendChild(button)
273
+ panel.appendChild(resizer)
274
+
275
+ makePanelDraggable(panel, handle)
276
+ makePanelResizable(panel, resizer)
277
+ applyPanelState(panel)
278
+
279
+ return panel
280
+ }
281
+
282
+ function closePanel(panel) {
283
+ writePanelState(panel)
284
+ setPanelClosed(true)
285
+ panel.remove()
286
+ }
287
+
288
+ function makePanelDraggable(panel, handle) {
289
+ handle.addEventListener('pointerdown', (event) => {
290
+ event.preventDefault()
291
+ panel.setPointerCapture?.(event.pointerId)
292
+
293
+ const rect = panel.getBoundingClientRect()
294
+ const startX = event.clientX
295
+ const startY = event.clientY
296
+ const startLeft = rect.left
297
+ const startTop = rect.top
298
+
299
+ const onMove = (moveEvent) => {
300
+ const left = startLeft + moveEvent.clientX - startX
301
+ const top = startTop + moveEvent.clientY - startY
302
+ panel.style.left = `${Math.round(left)}px`
303
+ panel.style.top = `${Math.round(top)}px`
304
+ panel.style.right = 'auto'
305
+ clampPanel(panel)
306
+ }
307
+
308
+ const onEnd = () => {
309
+ window.removeEventListener('pointermove', onMove)
310
+ window.removeEventListener('pointerup', onEnd)
311
+ window.removeEventListener('pointercancel', onEnd)
312
+ writePanelState(panel)
313
+ }
314
+
315
+ window.addEventListener('pointermove', onMove)
316
+ window.addEventListener('pointerup', onEnd)
317
+ window.addEventListener('pointercancel', onEnd)
318
+ })
319
+ }
320
+
321
+ function makePanelResizable(panel, resizer) {
322
+ resizer.addEventListener('pointerdown', (event) => {
323
+ event.preventDefault()
324
+ event.stopPropagation()
325
+ panel.setPointerCapture?.(event.pointerId)
326
+
327
+ const rect = panel.getBoundingClientRect()
328
+ const startX = event.clientX
329
+ const startY = event.clientY
330
+ const startWidth = rect.width
331
+ const startHeight = rect.height
332
+
333
+ const onMove = (moveEvent) => {
334
+ const width = clamp(startWidth + moveEvent.clientX - startX, 120, window.innerWidth)
335
+ const height = clamp(startHeight + moveEvent.clientY - startY, 58, window.innerHeight)
336
+ panel.style.width = `${Math.round(width)}px`
337
+ panel.style.height = `${Math.round(height)}px`
338
+ clampPanel(panel)
339
+ }
340
+
341
+ const onEnd = () => {
342
+ window.removeEventListener('pointermove', onMove)
343
+ window.removeEventListener('pointerup', onEnd)
344
+ window.removeEventListener('pointercancel', onEnd)
345
+ writePanelState(panel)
346
+ }
347
+
348
+ window.addEventListener('pointermove', onMove)
349
+ window.addEventListener('pointerup', onEnd)
350
+ window.addEventListener('pointercancel', onEnd)
351
+ })
352
+ }
353
+
354
+ function injectPanel() {
355
+ if (!document.body) {
356
+ return false
357
+ }
358
+
359
+ if (isPanelClosed()) {
360
+ return false
361
+ }
362
+
363
+ if (document.getElementById(PANEL_ID)) {
364
+ return true
365
+ }
366
+
367
+ ensureStyle()
368
+ document.body.appendChild(createPanel())
369
+ return true
370
+ }
371
+
372
+ function showPanel() {
373
+ setPanelClosed(false)
374
+ injectPanel()
375
+ const panel = document.getElementById(PANEL_ID)
376
+ if (panel) {
377
+ clampPanel(panel)
378
+ writePanelState(panel)
379
+ }
380
+ }
381
+
382
+ function startObserver() {
383
+ if (observer || !document.body) {
384
+ return
385
+ }
386
+
387
+ observer = new MutationObserver(() => {
388
+ injectPanel()
389
+ })
390
+ observer.observe(document.body, {
391
+ childList: true,
392
+ })
393
+ }
394
+
395
+ function startRetryTimer() {
396
+ if (retryTimer) {
397
+ return
398
+ }
399
+
400
+ retryTimer = window.setInterval(() => {
401
+ if (injectPanel() && retryTimer) {
402
+ window.clearInterval(retryTimer)
403
+ retryTimer = null
404
+ }
405
+ startObserver()
406
+ }, 1000)
407
+ }
408
+
409
+ function pluginDidLoad() {
410
+ if (document.readyState === 'loading') {
411
+ document.addEventListener('DOMContentLoaded', pluginDidLoad, { once: true })
412
+ return
413
+ }
414
+
415
+ injectPanel()
416
+ startObserver()
417
+ startRetryTimer()
418
+ window.addEventListener('resize', handleWindowResize)
419
+ window.addEventListener(SHOW_PANEL_EVENT, showPanel)
420
+ window.addEventListener(RELOAD_GAME_EVENT, reloadGame)
421
+ }
422
+
423
+ function handleWindowResize() {
424
+ const panel = document.getElementById(PANEL_ID)
425
+ if (panel) {
426
+ clampPanel(panel)
427
+ writePanelState(panel)
428
+ }
429
+ }
430
+
431
+ function pluginWillUnload() {
432
+ document.removeEventListener('DOMContentLoaded', pluginDidLoad)
433
+ window.removeEventListener('resize', handleWindowResize)
434
+ window.removeEventListener(SHOW_PANEL_EVENT, showPanel)
435
+ window.removeEventListener(RELOAD_GAME_EVENT, reloadGame)
436
+ document.getElementById(PANEL_ID)?.remove()
437
+ document.getElementById(STYLE_ID)?.remove()
438
+
439
+ if (observer) {
440
+ observer.disconnect()
441
+ observer = null
442
+ }
443
+
444
+ if (retryTimer) {
445
+ window.clearInterval(retryTimer)
446
+ retryTimer = null
447
+ }
448
+ }
449
+
450
+ module.exports = {
451
+ pluginDidLoad,
452
+ pluginWillUnload,
453
+ reactClass: (props) => require('./views').reactClass(props),
454
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "poi-plugin-reload-game-button",
3
+ "version": "0.3.0",
4
+ "description": "Adds a touch-friendly floating reload-game panel to poi.",
5
+ "main": "index.js",
6
+ "files": [
7
+ "README.md",
8
+ "index.js",
9
+ "views",
10
+ "i18n"
11
+ ],
12
+ "scripts": {
13
+ "test": "node test.js"
14
+ },
15
+ "keywords": [
16
+ "kancolle",
17
+ "poi",
18
+ "plugin",
19
+ "poi-plugin",
20
+ "reload",
21
+ "touch"
22
+ ],
23
+ "author": "DuskWhite",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/DuskWhite/poi-plugin-reload-game-button.git"
27
+ },
28
+ "homepage": "https://github.com/DuskWhite/poi-plugin-reload-game-button#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/DuskWhite/poi-plugin-reload-game-button/issues"
31
+ },
32
+ "license": "MIT",
33
+ "poiPlugin": {
34
+ "title": "重新载入游戏",
35
+ "description": "适合触摸屏使用的悬浮按钮,只重新载入游戏本体。",
36
+ "icon": "fa/refresh",
37
+ "priority": 10000,
38
+ "i18nDir": "i18n"
39
+ }
40
+ }
package/views/index.js ADDED
@@ -0,0 +1,107 @@
1
+ 'use strict'
2
+
3
+ const React = require('react')
4
+ const { Button, Callout, Intent } = require('@blueprintjs/core')
5
+ const { useTranslation } = require('react-i18next')
6
+
7
+ const SHOW_PANEL_EVENT = 'poi-plugin-reload-game-button:show-panel'
8
+ const RELOAD_GAME_EVENT = 'poi-plugin-reload-game-button:reload-game'
9
+ const PANEL_ID = 'poi-reload-game-panel'
10
+ const REPOSITORY_URL = 'https://github.com/DuskWhite/poi-plugin-reload-game-button'
11
+
12
+ function tt(t, key, fallback) {
13
+ const translated = t(key)
14
+ return translated === key ? fallback : translated
15
+ }
16
+
17
+ function reactClass() {
18
+ const { t } = useTranslation('poi-plugin-reload-game-button')
19
+ const [visible, setVisible] = React.useState(() => Boolean(document.getElementById(PANEL_ID)))
20
+
21
+ React.useEffect(() => {
22
+ const updateVisible = () => setVisible(Boolean(document.getElementById(PANEL_ID)))
23
+ const timer = window.setInterval(updateVisible, 1000)
24
+ window.addEventListener(SHOW_PANEL_EVENT, updateVisible)
25
+ return () => {
26
+ window.clearInterval(timer)
27
+ window.removeEventListener(SHOW_PANEL_EVENT, updateVisible)
28
+ }
29
+ }, [])
30
+
31
+ const showPanel = () => {
32
+ window.dispatchEvent(new Event(SHOW_PANEL_EVENT))
33
+ window.setTimeout(() => setVisible(Boolean(document.getElementById(PANEL_ID))), 0)
34
+ }
35
+
36
+ const reloadGame = () => {
37
+ window.dispatchEvent(new Event(RELOAD_GAME_EVENT))
38
+ }
39
+
40
+ return React.createElement(
41
+ 'div',
42
+ { style: { padding: 12 } },
43
+ React.createElement(
44
+ Callout,
45
+ {
46
+ intent: Intent.PRIMARY,
47
+ title: tt(t, 'title', '重新载入游戏'),
48
+ },
49
+ tt(t, 'description', '适合触摸屏使用的悬浮按钮,只重新载入游戏本体。'),
50
+ ),
51
+ React.createElement(
52
+ 'div',
53
+ {
54
+ style: {
55
+ display: 'flex',
56
+ gap: 8,
57
+ marginTop: 12,
58
+ },
59
+ },
60
+ React.createElement(
61
+ Button,
62
+ {
63
+ icon: 'widget',
64
+ intent: Intent.PRIMARY,
65
+ large: true,
66
+ onClick: showPanel,
67
+ },
68
+ tt(t, 'showPanel', '显示悬浮窗'),
69
+ ),
70
+ React.createElement(
71
+ Button,
72
+ {
73
+ icon: 'refresh',
74
+ intent: Intent.WARNING,
75
+ large: true,
76
+ onClick: reloadGame,
77
+ },
78
+ tt(t, 'reloadGame', '重新载入游戏'),
79
+ ),
80
+ ),
81
+ React.createElement(
82
+ 'div',
83
+ { style: { marginTop: 10, opacity: 0.78 } },
84
+ visible
85
+ ? tt(t, 'panelVisible', '悬浮窗已显示。')
86
+ : tt(t, 'panelHidden', '悬浮窗已隐藏。'),
87
+ ),
88
+ React.createElement(
89
+ 'div',
90
+ { style: { marginTop: 10 } },
91
+ `${tt(t, 'repository', '仓库地址')}: `,
92
+ React.createElement(
93
+ 'a',
94
+ {
95
+ href: REPOSITORY_URL,
96
+ onClick: (event) => {
97
+ event.preventDefault()
98
+ require('electron').shell.openExternal(REPOSITORY_URL)
99
+ },
100
+ },
101
+ REPOSITORY_URL,
102
+ ),
103
+ ),
104
+ )
105
+ }
106
+
107
+ exports.reactClass = reactClass