poi-plugin-kancolle-chs 1.3.8 → 1.4.2

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/index.es CHANGED
@@ -3,6 +3,7 @@ import { configManager } from './src/config'
3
3
  import { dataLoader } from './src/loaders/data-loader'
4
4
  import { mainPatcher } from './src/core/main-patcher'
5
5
  import { resourceManager } from './src/core/resource-manager'
6
+ import { startupChecker } from './src/core/startup-checker'
6
7
 
7
8
  // 导出插件在 POI 中展示的界面组件
8
9
  export const reactClass = MainPanel
@@ -31,7 +32,9 @@ export const pluginDidLoad = async () => {
31
32
  await mainPatcher.buildAndDeploy()
32
33
  }
33
34
 
34
- // 4. 若开启了启动自动检查更新,由 MainPanel 在渲染时自动检测并在有更新时弹窗提醒
35
+ // 4. 后台静默自检:即便用户未打开插件面板,也自动检查更新并在有更新时置顶弹窗提醒
36
+ startupChecker.scheduleStartupCheck(2000)
37
+
35
38
  console.log('[砍口垒汉化计划PLUS] 插件加载完毕,原生汉化模式已生效!')
36
39
  } else {
37
40
  console.log('[砍口垒汉化计划PLUS] 插件当前处于停用状态,确保环境纯净官方原版...')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-kancolle-chs",
3
- "version": "1.3.8",
3
+ "version": "1.4.2",
4
4
  "description": "砍口垒汉化计划PLUS POI 原生独立插件:提供全界面文本替换、正则动态替换、KCSAPI 响应合流、中文字体与贴图魔改、自定义字体适配与官方资源更新",
5
5
  "main": "index.es",
6
6
  "files": [
package/src/config.es CHANGED
@@ -93,8 +93,9 @@ class ConfigManager {
93
93
 
94
94
  if (saved && typeof saved === 'object') {
95
95
  this.config = { ...DEFAULT_CONFIG, ...saved }
96
- if (saved.autoCheckResourceUpdate === undefined && saved.autoCheckUpdate !== undefined) {
97
- this.config.autoCheckResourceUpdate = saved.autoCheckUpdate
96
+ // 自动保障:只要自动检查更新为开启状态,汉化资源自检与弹窗提醒同步开启
97
+ if (this.config.autoCheckUpdate !== false) {
98
+ this.config.autoCheckResourceUpdate = true
98
99
  }
99
100
  } else {
100
101
  // 若首次启动且无配置,尝试全平台推荐路径初始化
package/src/constants.es CHANGED
@@ -15,7 +15,7 @@ export const OFFICIAL_REPO_GIT = 'https://github.com/asaico/Kancolle_to_CHS.git'
15
15
  export const OFFICIAL_REPO_RELEASES = 'https://github.com/asaico/Kancolle_to_CHS/releases'
16
16
 
17
17
  // 插件版本与 npm 元信息
18
- let pkgVersion = '1.3.8'
18
+ let pkgVersion = '1.4.2'
19
19
  try {
20
20
  const pkg = require('../package.json')
21
21
  if (pkg && pkg.version) pkgVersion = pkg.version
@@ -0,0 +1,136 @@
1
+ import { configManager } from '../config'
2
+ import { resourceUpdater } from './resource-updater'
3
+ import { checkPluginUpdate } from './plugin-updater'
4
+ import { PLUGIN_VERSION } from '../constants'
5
+
6
+ /**
7
+ * POI 启动自检管理器 (单例)
8
+ * 在后台静默检查插件本体版本 (npm) 与本地汉化资源增量 (GitHub)。
9
+ * 检查结果同步派发给插件面板 (MainPanel),由插件内原生的暗黑风格 Blueprint Alert 统一弹窗展示,
10
+ * 彻底移除侵入式的 Windows 原生白色系统对话框。
11
+ */
12
+ class StartupChecker {
13
+ constructor() {
14
+ this.hasChecked = false
15
+ this.isChecking = false
16
+ this.lastResult = null
17
+ this.listeners = new Set()
18
+ }
19
+
20
+ subscribe(listener) {
21
+ this.listeners.add(listener)
22
+ return () => this.listeners.delete(listener)
23
+ }
24
+
25
+ notify() {
26
+ for (const fn of this.listeners) {
27
+ try {
28
+ fn(this.lastResult)
29
+ } catch (_) {}
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 启动时异步后台静默检查 (延迟 2 秒执行,避开 POI 启动窗口与网络初始化高峰)
35
+ */
36
+ scheduleStartupCheck(delayMs = 2000) {
37
+ if (this.hasChecked) return
38
+ setTimeout(() => {
39
+ this.checkOnStartup().catch((e) => {
40
+ console.warn('[KanCHS] 启动后台自检异常:', e)
41
+ })
42
+ }, delayMs)
43
+ }
44
+
45
+ /**
46
+ * 执行启动自检逻辑
47
+ */
48
+ async checkOnStartup() {
49
+ if (this.hasChecked || this.isChecking) return
50
+ this.isChecking = true
51
+ this.hasChecked = true
52
+
53
+ // 检查配置开关
54
+ const autoCheck = configManager.get('autoCheckUpdate', true) !== false &&
55
+ configManager.get('autoCheckResourceUpdate', true) !== false
56
+
57
+ if (!autoCheck) {
58
+ console.log('[KanCHS] 启动自检已在配置中关闭,跳过检查。')
59
+ this.isChecking = false
60
+ return
61
+ }
62
+
63
+ const sourceDir = configManager.resolveSourcePath()
64
+ const mirrorId = configManager.get('downloadMirror', 'ghproxy')
65
+
66
+ console.log('[KanCHS] 正在后台静默检查插件与汉化资源远端更新...')
67
+
68
+ try {
69
+ // 并发检查插件本体版本 (npm) 与本地汉化资源增量 (GitHub)
70
+ const [pluginRes, resourceRes] = await Promise.all([
71
+ checkPluginUpdate(PLUGIN_VERSION).catch((err) => {
72
+ console.warn('[KanCHS] 检查插件本体更新异常:', err.message)
73
+ return { hasUpdate: false, latestVersion: PLUGIN_VERSION }
74
+ }),
75
+ sourceDir
76
+ ? resourceUpdater.checkIncrementalUpdates({ sourceDir, mirrorId }).catch((err) => {
77
+ console.warn('[KanCHS] 检查汉化资源增量异常:', err.message)
78
+ return null
79
+ })
80
+ : null,
81
+ ])
82
+
83
+ const hasPlugin = Boolean(pluginRes && pluginRes.hasUpdate)
84
+ const hasResource = Boolean(
85
+ resourceRes &&
86
+ resourceRes.hasUpdate &&
87
+ Array.isArray(resourceRes.changedFiles) &&
88
+ resourceRes.changedFiles.length > 0
89
+ )
90
+
91
+ this.lastResult = {
92
+ pluginRes: pluginRes || { hasUpdate: false, latestVersion: PLUGIN_VERSION },
93
+ resourceRes,
94
+ hasPlugin,
95
+ hasResource,
96
+ }
97
+
98
+ this.notify()
99
+
100
+ if (hasPlugin || hasResource) {
101
+ console.log(`[KanCHS] 发现更新!插件更新: ${hasPlugin}, 资源变动: ${hasResource ? resourceRes.changedFiles.length + '项' : '无'}`)
102
+ this.notifyUpdateToast({
103
+ pluginRes: this.lastResult.pluginRes,
104
+ resourceRes,
105
+ hasPlugin,
106
+ hasResource,
107
+ })
108
+ } else {
109
+ console.log('[KanCHS] 启动自检完成:插件与汉化资源均为最新版本,保持完全静默。')
110
+ }
111
+ } catch (err) {
112
+ console.warn('[KanCHS] 启动检查失败:', err)
113
+ } finally {
114
+ this.isChecking = false
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 发送 POI 系统 Toast 提示(非侵入式,完全不使用原生白色对话框)
120
+ */
121
+ notifyUpdateToast({ pluginRes, resourceRes, hasPlugin, hasResource }) {
122
+ try {
123
+ if (typeof window !== 'undefined' && typeof window.toast === 'function') {
124
+ if (hasResource && hasPlugin) {
125
+ window.toast(`[砍口垒汉化] 发现插件 v${pluginRes.latestVersion} 及 ${resourceRes.changedFiles.length} 项资源更新,已在面板就绪!`, { type: 'warning' })
126
+ } else if (hasResource) {
127
+ window.toast(`[砍口垒汉化] 发现 ${resourceRes.changedFiles.length} 项汉化资源可更新,已在面板就绪!`, { type: 'warning' })
128
+ } else if (hasPlugin) {
129
+ window.toast(`[砍口垒汉化] 发现插件新版本 v${pluginRes.latestVersion},已在面板就绪!`, { type: 'warning' })
130
+ }
131
+ }
132
+ } catch (_) {}
133
+ }
134
+ }
135
+
136
+ export const startupChecker = new StartupChecker()
@@ -39,6 +39,7 @@ import {
39
39
  NPM_PACKAGE_URL,
40
40
  } from '../constants'
41
41
  import { checkPluginUpdate } from '../core/plugin-updater'
42
+ import { startupChecker } from '../core/startup-checker'
42
43
 
43
44
  export class MainPanel extends Component {
44
45
  rootRef = React.createRef()
@@ -92,6 +93,8 @@ export class MainPanel extends Component {
92
93
  showUnconfiguredAlert: false, // 启用汉化时资源包未配置提示弹窗
93
94
  showRestartConfirm: false, // 重启 POI 客户端二次确认弹窗
94
95
  showResourceUpdateAlert: false, // 启动或检查检测到本地汉化资源有更新提醒弹窗
96
+ showPluginUpdateAlert: false, // 启动或检查检测到插件本体有新版本更新提醒弹窗
97
+ showPostUpdateRestartAlert: false, // 资源更新完成后提示是否自动清缓存并重启弹窗
95
98
  logs: [],
96
99
  }
97
100
  }
@@ -121,33 +124,78 @@ export class MainPanel extends Component {
121
124
  }).catch(() => {})
122
125
  }
123
126
 
124
- // 启动时自动检查本地汉化资源更新 (支持弹窗提醒)
125
- const shouldCheckResource = this.state.config.autoCheckResourceUpdate !== undefined
126
- ? this.state.config.autoCheckResourceUpdate
127
- : this.state.config.autoCheckUpdate
128
-
129
- if (shouldCheckResource && !this.hasAutoCheckedUpdates) {
130
- this.hasAutoCheckedUpdates = true
131
- this.handleCheckIncrementalUpdates({ isAutoStartup: true }).catch(() => {})
132
- }
127
+ // 监听与同步全局启动自检结果 (由 index.es 或 MainPanel 触发)
128
+ this.unsubStartupChecker = startupChecker.subscribe((res) => {
129
+ if (res) {
130
+ this.setState({
131
+ pluginUpdate: res.pluginRes || this.state.pluginUpdate,
132
+ resourceUpdaterStatus: {
133
+ ...this.state.resourceUpdaterStatus,
134
+ hasIncrementalUpdate: res.hasResource,
135
+ changedFiles: res.resourceRes?.changedFiles || [],
136
+ },
137
+ showResourceUpdateAlert: res.hasResource,
138
+ showPluginUpdateAlert: res.hasPlugin,
139
+ })
140
+ }
141
+ })
133
142
 
134
- // 自动在后台静默检查插件本体版本更新
135
- if (this.state.config.autoCheckUpdate) {
136
- this.handleCheckPluginUpdate(true).catch(() => {})
143
+ if (startupChecker.lastResult) {
144
+ const res = startupChecker.lastResult
145
+ this.setState({
146
+ pluginUpdate: res.pluginRes || this.state.pluginUpdate,
147
+ resourceUpdaterStatus: {
148
+ ...this.state.resourceUpdaterStatus,
149
+ hasIncrementalUpdate: res.hasResource,
150
+ changedFiles: res.resourceRes?.changedFiles || [],
151
+ },
152
+ showResourceUpdateAlert: res.hasResource,
153
+ showPluginUpdateAlert: res.hasPlugin,
154
+ })
155
+ } else {
156
+ const shouldAutoCheck = this.state.config.autoCheckUpdate !== false
157
+ if (shouldAutoCheck && !startupChecker.hasChecked) {
158
+ startupChecker.scheduleStartupCheck(500)
159
+ }
137
160
  }
138
161
 
139
162
  // 针对旧版 POI 或特定主题外层容器修正高度与滚动上下文
140
163
  this.fixParentScrollStyles()
164
+ setTimeout(this.fixParentScrollStyles, 50)
165
+ setTimeout(this.fixParentScrollStyles, 250)
166
+
167
+ const rootEl = this.rootRef.current
168
+ if (rootEl) {
169
+ rootEl.addEventListener('wheel', this.handleWheel, { passive: false })
170
+
171
+ const doc = rootEl.ownerDocument || document
172
+ const win = doc.defaultView || (typeof window !== 'undefined' ? window : null)
173
+ this.currentDoc = doc
174
+ this.currentWin = win
175
+
176
+ if (win) {
177
+ win.addEventListener('resize', this.handleResize)
178
+ if (win !== window) {
179
+ // 独立新窗口模式:在 window 与 document 均挂载滚轮监听,实现全屏无死角即时滚动响应
180
+ win.addEventListener('wheel', this.handleWheel, { passive: false })
181
+ doc.addEventListener('wheel', this.handleWheel, { passive: false })
182
+ }
183
+ }
141
184
 
142
- // 绑定原生滚轮双重补偿监听 (保障老旧 Electron 或卡片事件捕获异常环境)
143
- if (this.rootRef && this.rootRef.current) {
144
- this.rootRef.current.addEventListener('wheel', this.handleWheel, { passive: true })
185
+ // 确保进入新窗口时容器立即可获得焦点与滚动上下文
186
+ setTimeout(() => {
187
+ try {
188
+ rootEl.focus({ preventScroll: true })
189
+ } catch (_) {}
190
+ }, 50)
145
191
  }
146
192
 
147
193
  // 监听窗口与容器自适应宽度变化,实现动态窄窗紧凑排版适配
148
194
  this.handleResize = () => {
149
195
  if (this.rootRef && this.rootRef.current) {
150
196
  this.setState({ containerWidth: this.rootRef.current.offsetWidth })
197
+ } else if (this.currentWin) {
198
+ this.setState({ containerWidth: this.currentWin.innerWidth })
151
199
  } else if (typeof window !== 'undefined') {
152
200
  this.setState({ containerWidth: window.innerWidth })
153
201
  }
@@ -205,27 +253,65 @@ export class MainPanel extends Component {
205
253
  if (this.pluginUpdateNoticeTimer) clearTimeout(this.pluginUpdateNoticeTimer)
206
254
  if (this.unsubConfig) this.unsubConfig()
207
255
  if (this.unsubGit) this.unsubGit()
256
+ if (this.unsubStartupChecker) this.unsubStartupChecker()
208
257
  if (typeof window !== 'undefined') {
209
258
  window.removeEventListener('resize', this.handleResize)
210
259
  }
211
260
  if (this.rootRef && this.rootRef.current) {
212
261
  this.rootRef.current.removeEventListener('wheel', this.handleWheel)
213
262
  }
263
+ if (this.currentWin) {
264
+ this.currentWin.removeEventListener('resize', this.handleResize)
265
+ if (this.currentWin !== window) {
266
+ this.currentWin.removeEventListener('wheel', this.handleWheel)
267
+ }
268
+ }
269
+ if (this.currentDoc && this.currentDoc !== document) {
270
+ this.currentDoc.removeEventListener('wheel', this.handleWheel)
271
+ }
214
272
  if (this.ro) {
215
273
  this.ro.disconnect()
216
274
  }
217
275
  }
218
276
 
219
277
  /**
220
- * 自动向上递归修正 POI 外层 Tab 容器的高度与 flex 布局,确保内层滚动条与滚轮绝对可用
278
+ * 自动向上递归修正 POI 外层 Tab 及独立新窗口容器的高度与 flex 布局,确保内层滚动条与滚轮绝对可用
221
279
  */
222
280
  fixParentScrollStyles = () => {
223
281
  try {
224
282
  const root = this.rootRef.current
225
283
  if (!root) return
284
+
285
+ const doc = root.ownerDocument || document
286
+ const win = doc.defaultView || window
287
+ const isDetachedWindow = (doc !== document) || (win !== window) || Boolean(doc.getElementById('plugin-mountpoint'))
288
+
289
+ // 1. 若处于独立新窗口模式,修正 html、body 及 #plugin-mountpoint 的高度与溢出策略
290
+ if (isDetachedWindow) {
291
+ if (doc.documentElement) {
292
+ if (doc.documentElement.style.height !== '100%') doc.documentElement.style.height = '100%'
293
+ if (doc.documentElement.style.overflow !== 'hidden') doc.documentElement.style.overflow = 'hidden'
294
+ }
295
+ if (doc.body) {
296
+ if (doc.body.style.height !== '100%') doc.body.style.height = '100%'
297
+ if (doc.body.style.margin !== '0px') doc.body.style.margin = '0'
298
+ if (doc.body.style.padding !== '0px') doc.body.style.padding = '0'
299
+ if (doc.body.style.overflow !== 'hidden') doc.body.style.overflow = 'hidden'
300
+ }
301
+ const mountpoint = doc.getElementById('plugin-mountpoint')
302
+ if (mountpoint) {
303
+ if (mountpoint.style.height !== '100vh') mountpoint.style.height = '100vh'
304
+ if (mountpoint.style.maxHeight !== '100vh') mountpoint.style.maxHeight = '100vh'
305
+ if (mountpoint.style.display !== 'flex') mountpoint.style.display = 'flex'
306
+ if (mountpoint.style.flexDirection !== 'column') mountpoint.style.flexDirection = 'column'
307
+ if (mountpoint.style.overflow !== 'hidden') mountpoint.style.overflow = 'hidden'
308
+ }
309
+ }
310
+
311
+ // 2. 向上递归修正所有外层容器的高度与 flex 布局
226
312
  let parent = root.parentElement
227
313
  let depth = 0
228
- while (parent && depth < 4) {
314
+ while (parent && depth < 6) {
229
315
  const id = parent.id || ''
230
316
  const cls = parent.className || ''
231
317
  if (
@@ -235,9 +321,17 @@ export class MainPanel extends Component {
235
321
  id.includes('poi-plugin-kancolle-chs') ||
236
322
  id === 'plugin-mountpoint'
237
323
  ) {
238
- if (parent.style.height !== '100%') parent.style.height = '100%'
324
+ if (id === 'plugin-mountpoint') {
325
+ if (parent.style.height !== '100vh') parent.style.height = '100vh'
326
+ if (parent.style.maxHeight !== '100vh') parent.style.maxHeight = '100vh'
327
+ } else {
328
+ if (parent.style.height !== '100%') parent.style.height = '100%'
329
+ if (parent.style.maxHeight !== '100%') parent.style.maxHeight = '100%'
330
+ }
239
331
  if (parent.style.display !== 'flex') parent.style.display = 'flex'
240
332
  if (parent.style.flexDirection !== 'column') parent.style.flexDirection = 'column'
333
+ if (parent.style.flex !== '1 1 0px' && parent.style.flex !== '1') parent.style.flex = '1 1 0'
334
+ if (parent.style.minHeight !== '0px') parent.style.minHeight = '0'
241
335
  if (parent.style.overflow !== 'hidden') parent.style.overflow = 'hidden'
242
336
  }
243
337
  parent = parent.parentElement
@@ -247,20 +341,50 @@ export class MainPanel extends Component {
247
341
  }
248
342
 
249
343
  /**
250
- * 滚轮平滑滚动双重补偿兜底
344
+ * 滚轮平滑滚动双重补偿与新窗口即时响应机制
251
345
  */
252
346
  handleWheel = (e) => {
253
347
  const container = this.rootRef.current
254
348
  if (!container) return
255
- if (container.scrollHeight <= container.clientHeight) return
256
349
 
257
- // 双重保障:若在老版本 POI / Electron 中原生滚轮事件未驱动 scrollTop,进行位移补偿
258
- const prevScrollTop = container.scrollTop
259
- requestAnimationFrame(() => {
260
- if (container && container.scrollTop === prevScrollTop && e.deltaY !== 0) {
261
- container.scrollTop += e.deltaY
262
- }
263
- })
350
+ // 如果事件目标是拥有独立滚动条的子元素(如 textarea),不予干涉
351
+ const target = e.target
352
+ if (target && (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT')) {
353
+ if (target.scrollHeight > target.clientHeight) return
354
+ }
355
+
356
+ // 获取当前所属窗口(若在新窗口打开,则使用该独立窗口的 window 与 rAF)
357
+ const win = container.ownerDocument?.defaultView || (typeof window !== 'undefined' ? window : null)
358
+ const isDetachedWindow = (container.ownerDocument !== document) || (win && win !== window)
359
+
360
+ // 计算滚轮位移量
361
+ let delta = e.deltaY
362
+ if (e.deltaMode === 1) delta *= 25
363
+ else if (e.deltaMode === 2) delta *= container.clientHeight
364
+
365
+ if (container.scrollHeight <= container.clientHeight) {
366
+ // 容错重试:若尚未修正外层样式导致高度膨胀,立即修正一次
367
+ this.fixParentScrollStyles()
368
+ if (container.scrollHeight <= container.clientHeight) return
369
+ }
370
+
371
+ if (isDetachedWindow) {
372
+ // 独立新窗口模式:直接驱动 scrollTop 确保 100% 极速平滑滚动响应
373
+ container.scrollTop += delta
374
+ if (e.cancelable) e.preventDefault()
375
+ } else {
376
+ // 常规主窗口 Tab:双重补偿保障机制
377
+ const prevScrollTop = container.scrollTop
378
+ const rAF = (win && typeof win.requestAnimationFrame === 'function')
379
+ ? win.requestAnimationFrame.bind(win)
380
+ : requestAnimationFrame
381
+
382
+ rAF(() => {
383
+ if (container && container.scrollTop === prevScrollTop && delta !== 0) {
384
+ container.scrollTop += delta
385
+ }
386
+ })
387
+ }
264
388
  }
265
389
 
266
390
  /**
@@ -510,6 +634,22 @@ export class MainPanel extends Component {
510
634
  }
511
635
  }
512
636
 
637
+ /**
638
+ * 清除游戏缓存并自动重启 POI 客户端 (更新完成后一键执行)
639
+ */
640
+ executeClearCacheAndRestartPoi = async () => {
641
+ this.setState({ showPostUpdateRestartAlert: false })
642
+ this.addLog('正在全方位清理游戏底层缓存与会话数据...')
643
+ try {
644
+ await resourceManager.clearGameCache()
645
+ this.addLog('游戏缓存清理完成,即将自动重启 POI 客户端...')
646
+ } catch (e) {
647
+ console.warn('清理游戏缓存异常:', e)
648
+ this.addLog(`清理游戏缓存轻微异常: ${e.message},继续执行重启...`)
649
+ }
650
+ this.executeRestartPoi()
651
+ }
652
+
513
653
  /**
514
654
  * 点击切换总开关 (弹出二次确认弹窗,防止用户误触并支持自动清缓存刷新)
515
655
  */
@@ -977,6 +1117,81 @@ export class MainPanel extends Component {
977
1117
  }
978
1118
  }
979
1119
 
1120
+ /**
1121
+ * 一键检查插件和汉化资源的更新
1122
+ * @param {boolean} silent 是否静默检查 (后台自动检查不显示全局状态文字,但若有资源更新依旧弹窗)
1123
+ */
1124
+ handleCheckAllUpdates = async (silent = false) => {
1125
+ this.setState({ isCheckingPluginUpdate: true })
1126
+ if (!silent) {
1127
+ this.setState({
1128
+ statusMessage: '正在一键检查插件本体与汉化资源更新...',
1129
+ statusIntent: Intent.PRIMARY,
1130
+ })
1131
+ this.addLog('正在一键检查插件本体(npm)与汉化资源包(GitHub)远端更新...')
1132
+ }
1133
+
1134
+ try {
1135
+ // 并发执行:1. 插件 npm 版本检查 2. 汉化资源增量文件哈希比对
1136
+ const [pluginRes] = await Promise.all([
1137
+ checkPluginUpdate(PLUGIN_VERSION).catch((e) => {
1138
+ this.addLog(`检查插件更新失败: ${e.message}`)
1139
+ return { hasUpdate: false, latestVersion: PLUGIN_VERSION }
1140
+ }),
1141
+ this.handleCheckIncrementalUpdates({
1142
+ isAutoStartup: silent,
1143
+ showAlert: true,
1144
+ }).catch((e) => {
1145
+ this.addLog(`检查汉化资源更新失败: ${e.message}`)
1146
+ return null
1147
+ }),
1148
+ ])
1149
+
1150
+ this.setState({
1151
+ pluginUpdate: pluginRes || { hasUpdate: false, latestVersion: PLUGIN_VERSION },
1152
+ isCheckingPluginUpdate: false,
1153
+ hidePluginUpdateNotice: false,
1154
+ })
1155
+
1156
+ if (pluginRes && pluginRes.hasUpdate) {
1157
+ this.addLog(`🎉 发现插件新版本: v${pluginRes.latestVersion} (当前已安装: v${PLUGIN_VERSION})`)
1158
+ this.setState({ showPluginUpdateAlert: true })
1159
+ if (!silent) {
1160
+ this.setState({
1161
+ statusMessage: `🎉 发现插件新版本 v${pluginRes.latestVersion}!请前往 POI「设置 - 插件」面板更新`,
1162
+ statusIntent: Intent.WARNING,
1163
+ })
1164
+ }
1165
+ } else {
1166
+ this.addLog(`插件本体当前已是最新版本 (v${PLUGIN_VERSION})`)
1167
+ }
1168
+
1169
+ if (!silent) {
1170
+ const hasResUpdate = Boolean(
1171
+ this.state.resourceUpdaterStatus &&
1172
+ this.state.resourceUpdaterStatus.hasIncrementalUpdate &&
1173
+ this.state.resourceUpdaterStatus.changedFiles &&
1174
+ this.state.resourceUpdaterStatus.changedFiles.length > 0
1175
+ )
1176
+ if (!pluginRes?.hasUpdate && !hasResUpdate) {
1177
+ this.setState({
1178
+ statusMessage: `✅ 检查完成:插件本体与汉化资源包均为最新版本!`,
1179
+ statusIntent: Intent.SUCCESS,
1180
+ })
1181
+ }
1182
+ }
1183
+ } catch (err) {
1184
+ this.setState({
1185
+ isCheckingPluginUpdate: false,
1186
+ statusMessage: `检查更新失败: ${err.message}`,
1187
+ statusIntent: Intent.DANGER,
1188
+ })
1189
+ this.addLog(`一键检查更新异常: ${err.message}`)
1190
+ } finally {
1191
+ this.setState({ isCheckingPluginUpdate: false })
1192
+ }
1193
+ }
1194
+
980
1195
  /**
981
1196
  * 极度健壮的打开本地文件夹方法 (优先 Electron openPath,兜底 Windows explorer)
982
1197
  */
@@ -1240,6 +1455,7 @@ export class MainPanel extends Component {
1240
1455
  isUpdating: false,
1241
1456
  updateType: null,
1242
1457
  },
1458
+ showPostUpdateRestartAlert: true,
1243
1459
  })
1244
1460
  await this.refreshAllStatus()
1245
1461
  } catch (e) {
@@ -1280,12 +1496,13 @@ export class MainPanel extends Component {
1280
1496
  */
1281
1497
  handleCheckIncrementalUpdates = async (options = {}) => {
1282
1498
  const isAutoStartup = Boolean(options && options.isAutoStartup)
1499
+ const showAlert = options && options.showAlert !== undefined ? Boolean(options.showAlert) : true
1283
1500
  const sourceDir = configManager.resolveSourcePath()
1284
1501
  if (!sourceDir) {
1285
1502
  if (!isAutoStartup) {
1286
1503
  this.setState({ statusMessage: '尚未配置汉化资源包路径,请先指定或下载资源包', statusIntent: Intent.WARNING })
1287
1504
  }
1288
- return
1505
+ return null
1289
1506
  }
1290
1507
 
1291
1508
  if (!isAutoStartup) {
@@ -1307,11 +1524,11 @@ export class MainPanel extends Component {
1307
1524
  hasIncrementalUpdate: true,
1308
1525
  changedFiles: res.changedFiles,
1309
1526
  },
1310
- showResourceUpdateAlert: false,
1311
- statusMessage: `检测到 ${res.changedFiles.length} 个文件与远端有差异`,
1312
- statusIntent: Intent.NONE,
1527
+ showResourceUpdateAlert: showAlert,
1528
+ statusMessage: `🎉 检测到 ${res.changedFiles.length} 个汉化资源文件可更新`,
1529
+ statusIntent: Intent.WARNING,
1313
1530
  })
1314
- this.addLog(`检测到 ${res.changedFiles.length} 个汉化资源文件与远端有差异(已记录,不弹窗打扰)`)
1531
+ this.addLog(`🎉 检测到 ${res.changedFiles.length} 个汉化资源文件与远端有差异${showAlert ? ',已弹出更新提醒窗口' : ''}`)
1315
1532
  } else {
1316
1533
  this.setState({
1317
1534
  resourceUpdaterStatus: {
@@ -1326,6 +1543,7 @@ export class MainPanel extends Component {
1326
1543
  this.addLog('启动自检完成:本地汉化资源已是最新版本,无变动文件')
1327
1544
  }
1328
1545
  }
1546
+ return res
1329
1547
  } catch (e) {
1330
1548
  if (!isAutoStartup) {
1331
1549
  this.setState({
@@ -1334,6 +1552,7 @@ export class MainPanel extends Component {
1334
1552
  })
1335
1553
  }
1336
1554
  this.addLog(`检查汉化资源变动失败: ${e.message}`)
1555
+ return null
1337
1556
  } finally {
1338
1557
  if (!isAutoStartup) {
1339
1558
  this.setState({ isProcessing: false })
@@ -1383,6 +1602,7 @@ export class MainPanel extends Component {
1383
1602
  },
1384
1603
  statusMessage: '轻量化更新成功!所有变动词条已自动合流并重新部署生效',
1385
1604
  statusIntent: Intent.SUCCESS,
1605
+ showPostUpdateRestartAlert: true,
1386
1606
  })
1387
1607
  await this.refreshAllStatus()
1388
1608
  } catch (e) {
@@ -1701,6 +1921,8 @@ export class MainPanel extends Component {
1701
1921
  showTimeoutAlert,
1702
1922
  showRestartConfirm,
1703
1923
  showResourceUpdateAlert,
1924
+ showPluginUpdateAlert,
1925
+ showPostUpdateRestartAlert,
1704
1926
  logs,
1705
1927
  } = this.state
1706
1928
 
@@ -1745,8 +1967,9 @@ export class MainPanel extends Component {
1745
1967
  style={{
1746
1968
  width: '100%',
1747
1969
  height: '100%',
1748
- minHeight: '100%',
1970
+ minHeight: 0,
1749
1971
  maxHeight: '100%',
1972
+ flex: '1 1 0',
1750
1973
  overflowY: 'auto',
1751
1974
  overflowX: 'hidden',
1752
1975
  WebkitOverflowScrolling: 'touch',
@@ -1848,22 +2071,42 @@ export class MainPanel extends Component {
1848
2071
  }
1849
2072
  }
1850
2073
 
2074
+ /* 新窗口与常规 Tab 模式全层级自适应滚动链保障 */
2075
+ html, body {
2076
+ height: 100% !important;
2077
+ overflow: hidden !important;
2078
+ }
2079
+ #plugin-mountpoint {
2080
+ height: 100vh !important;
2081
+ max-height: 100vh !important;
2082
+ display: flex !important;
2083
+ flex-direction: column !important;
2084
+ overflow: hidden !important;
2085
+ box-sizing: border-box !important;
2086
+ }
2087
+ .poi-app-tabpane,
1851
2088
  #poi-plugin-kancolle-chs,
1852
2089
  #poi-plugin-kancolle-chs.poi-app-tabpane,
1853
2090
  .poi-plugin-content-poi-plugin-kancolle-chs {
1854
2091
  height: 100% !important;
2092
+ max-height: 100% !important;
1855
2093
  display: flex !important;
2094
+ flex: 1 1 0 !important;
2095
+ min-height: 0 !important;
1856
2096
  flex-direction: column !important;
1857
2097
  overflow: hidden !important;
2098
+ box-sizing: border-box !important;
1858
2099
  }
1859
2100
  .kancolle-chs-plus-scroll-container {
1860
2101
  height: 100% !important;
1861
- min-height: 100% !important;
2102
+ min-height: 0 !important;
1862
2103
  max-height: 100% !important;
2104
+ flex: 1 1 0 !important;
1863
2105
  overflow-y: auto !important;
1864
2106
  overflow-x: hidden !important;
1865
2107
  -webkit-overflow-scrolling: touch !important;
1866
2108
  outline: none !important;
2109
+ box-sizing: border-box !important;
1867
2110
  }
1868
2111
  .kancolle-chs-plus-scroll-container::-webkit-scrollbar {
1869
2112
  width: 8px;
@@ -1918,11 +2161,21 @@ export class MainPanel extends Component {
1918
2161
  small
1919
2162
  minimal
1920
2163
  icon="refresh"
1921
- title="通过 npm 检查插件最新发布版本"
1922
- text={isCheckingPluginUpdate ? '检查中...' : (pluginUpdate.hasUpdate ? `发现新版 v${pluginUpdate.latestVersion}!` : '检查更新')}
1923
- intent={pluginUpdate.hasUpdate ? Intent.WARNING : Intent.NONE}
2164
+ title="一键检查插件本体(npm)与汉化资源包(GitHub)更新"
2165
+ text={
2166
+ isCheckingPluginUpdate
2167
+ ? '检查中...'
2168
+ : (pluginUpdate.hasUpdate && resourceUpdaterStatus.hasIncrementalUpdate)
2169
+ ? '发现更新! (插件+资源)'
2170
+ : pluginUpdate.hasUpdate
2171
+ ? `插件新版 v${pluginUpdate.latestVersion}!`
2172
+ : resourceUpdaterStatus.hasIncrementalUpdate
2173
+ ? `资源可更新 (${(resourceUpdaterStatus.changedFiles || []).length})`
2174
+ : (isCompact ? '检查更新' : '一键检查更新')
2175
+ }
2176
+ intent={(pluginUpdate.hasUpdate || resourceUpdaterStatus.hasIncrementalUpdate) ? Intent.WARNING : Intent.NONE}
1924
2177
  loading={isCheckingPluginUpdate}
1925
- onClick={() => this.handleCheckPluginUpdate(false)}
2178
+ onClick={() => this.handleCheckAllUpdates(false)}
1926
2179
  />
1927
2180
  <Button
1928
2181
  small
@@ -2427,9 +2680,19 @@ export class MainPanel extends Component {
2427
2680
 
2428
2681
  <div style={{ marginTop: '8px' }}>
2429
2682
  <Switch
2430
- label="启动 POI 时自动检查本地汉化资源更新并在面板提示"
2431
- checked={config.autoCheckResourceUpdate !== undefined ? config.autoCheckResourceUpdate : config.autoCheckUpdate}
2432
- onChange={() => this.handleToggleOption('autoCheckResourceUpdate')}
2683
+ label="每次启动 POI 时静默自动检查更新并在有更新时弹窗提示"
2684
+ checked={config.autoCheckResourceUpdate !== false && config.autoCheckUpdate !== false}
2685
+ onChange={() => {
2686
+ const current = config.autoCheckResourceUpdate !== false && config.autoCheckUpdate !== false
2687
+ configManager.update({
2688
+ autoCheckUpdate: !current,
2689
+ autoCheckResourceUpdate: !current,
2690
+ })
2691
+ this.setState({
2692
+ statusMessage: `已${!current ? '开启' : '关闭'}每次启动 POI 自动检查更新提醒`,
2693
+ statusIntent: Intent.SUCCESS,
2694
+ })
2695
+ }}
2433
2696
  />
2434
2697
  </div>
2435
2698
  </div>
@@ -3422,6 +3685,139 @@ export class MainPanel extends Component {
3422
3685
  </div>
3423
3686
  </div>
3424
3687
  </Alert>
3688
+
3689
+ {/* 启动与一键检查发现更新时的统一提醒弹窗 (资源更新 / 插件更新 / 两者皆有) */}
3690
+ <Alert
3691
+ isOpen={showResourceUpdateAlert || showPluginUpdateAlert}
3692
+ cancelButtonText="稍后再说"
3693
+ confirmButtonText={
3694
+ resourceUpdaterStatus.hasIncrementalUpdate
3695
+ ? (isCompact ? '立即更新资源' : `⚡ 立即轻量更新资源 (${(resourceUpdaterStatus.changedFiles || []).length}项)`)
3696
+ : '查看插件发布页'
3697
+ }
3698
+ intent={Intent.PRIMARY}
3699
+ icon={pluginUpdate.hasUpdate && resourceUpdaterStatus.hasIncrementalUpdate ? 'notifications' : (resourceUpdaterStatus.hasIncrementalUpdate ? 'cloud-download' : 'updated')}
3700
+ onCancel={() => this.setState({ showResourceUpdateAlert: false, showPluginUpdateAlert: false })}
3701
+ onConfirm={() => {
3702
+ const hasRes = resourceUpdaterStatus.hasIncrementalUpdate
3703
+ this.setState({ showResourceUpdateAlert: false, showPluginUpdateAlert: false })
3704
+ if (hasRes) {
3705
+ this.handleApplyIncrementalUpdates()
3706
+ } else {
3707
+ this.handleOpenExternal(pluginUpdate.npmUrl || NPM_PACKAGE_URL)
3708
+ }
3709
+ }}
3710
+ canOutsideClickCancel={true}
3711
+ canEscapeKeyCancel={true}
3712
+ className={isDarkTheme ? 'bp3-dark bp4-dark kancolle-chs-dark-theme' : ''}
3713
+ style={{ maxWidth: '540px' }}
3714
+ >
3715
+ <div style={{ lineHeight: '1.6' }}>
3716
+ <h5 style={{ margin: '0 0 10px 0', fontSize: '15px', fontWeight: 'bold' }}>
3717
+ {pluginUpdate.hasUpdate && resourceUpdaterStatus.hasIncrementalUpdate
3718
+ ? '🎉 发现插件与汉化资源均有新更新!'
3719
+ : pluginUpdate.hasUpdate
3720
+ ? '🎉 发现砍口垒汉化计划PLUS插件新版本!'
3721
+ : '📦 发现本地汉化资源有新更新!'}
3722
+ </h5>
3723
+
3724
+ {/* 插件本体更新板块 */}
3725
+ {pluginUpdate.hasUpdate && (
3726
+ <div
3727
+ style={{
3728
+ padding: '10px 12px',
3729
+ background: 'rgba(16, 107, 163, 0.08)',
3730
+ borderRadius: '6px',
3731
+ borderLeft: '4px solid #106ba3',
3732
+ marginBottom: resourceUpdaterStatus.hasIncrementalUpdate ? '12px' : '8px',
3733
+ }}
3734
+ >
3735
+ <div style={{ fontWeight: 'bold', fontSize: '13px', color: '#106ba3' }}>
3736
+ 🚀 插件本体新版本: v{pluginUpdate.latestVersion}(当前已安装: v{PLUGIN_VERSION})
3737
+ </div>
3738
+ <div style={{ fontSize: '12px', color: '#444', marginTop: '4px' }}>
3739
+ 更新方法:点击 POI 顶部菜单【设置】➔【插件】➔ 找到【砍口垒汉化计划PLUS】点击【更新】即可。
3740
+ </div>
3741
+ </div>
3742
+ )}
3743
+
3744
+ {/* 汉化资源更新板块 */}
3745
+ {resourceUpdaterStatus.hasIncrementalUpdate && (
3746
+ <div>
3747
+ <p style={{ margin: '0 0 8px 0', fontSize: '13px' }}>
3748
+ 经比对官方汉化仓库,检测到本地有 <strong>{(resourceUpdaterStatus.changedFiles || []).length}</strong> 个汉化文件可更新:
3749
+ </p>
3750
+ {(resourceUpdaterStatus.changedFiles || []).length > 0 && (
3751
+ <div
3752
+ style={{
3753
+ maxHeight: '130px',
3754
+ overflowY: 'auto',
3755
+ background: 'rgba(0,0,0,0.03)',
3756
+ padding: '8px 10px',
3757
+ borderRadius: '6px',
3758
+ border: '1px solid rgba(0,0,0,0.08)',
3759
+ marginBottom: '10px',
3760
+ fontSize: '12px',
3761
+ fontFamily: 'monospace',
3762
+ }}
3763
+ >
3764
+ <div style={{ fontWeight: 'bold', marginBottom: '4px', color: '#0d8050' }}>变动文件列表:</div>
3765
+ {(resourceUpdaterStatus.changedFiles || []).slice(0, 8).map((f, idx) => (
3766
+ <div key={idx} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
3767
+ • {typeof f === 'string' ? f : (f.path || f.file || JSON.stringify(f))}
3768
+ </div>
3769
+ ))}
3770
+ {(resourceUpdaterStatus.changedFiles || []).length > 8 && (
3771
+ <div style={{ color: '#888', fontStyle: 'italic', marginTop: '2px' }}>
3772
+ ... 等共 {(resourceUpdaterStatus.changedFiles || []).length} 个文件
3773
+ </div>
3774
+ )}
3775
+ </div>
3776
+ )}
3777
+ <div style={{ fontSize: '12px', color: '#666' }}>
3778
+ 💡 推荐执行轻量增量更新,仅下载替换变动文件,耗时极短且无需重新下载完整资源包。
3779
+ </div>
3780
+ </div>
3781
+ )}
3782
+ </div>
3783
+ </Alert>
3784
+
3785
+ {/* 汉化资源更新完成后的清缓存并重启确认弹窗 */}
3786
+ <Alert
3787
+ isOpen={showPostUpdateRestartAlert}
3788
+ cancelButtonText="稍后手动重启"
3789
+ confirmButtonText="⚡ 清缓存并重启 POI"
3790
+ intent={Intent.PRIMARY}
3791
+ icon="refresh"
3792
+ onCancel={() => this.setState({ showPostUpdateRestartAlert: false })}
3793
+ onConfirm={this.executeClearCacheAndRestartPoi}
3794
+ canOutsideClickCancel={true}
3795
+ canEscapeKeyCancel={true}
3796
+ className={isDarkTheme ? 'bp3-dark bp4-dark kancolle-chs-dark-theme' : ''}
3797
+ style={{ maxWidth: '480px' }}
3798
+ >
3799
+ <div style={{ lineHeight: '1.6' }}>
3800
+ <h5 style={{ margin: '0 0 10px 0', fontSize: '15px', fontWeight: 'bold' }}>
3801
+ 🎉 汉化资源已更新完成!
3802
+ </h5>
3803
+ <p style={{ margin: '0 0 10px 0', fontSize: '13px' }}>
3804
+ 所有汉化资源已成功下载并自动完成合流注入。<br />
3805
+ 是否立即<strong>自动清理游戏缓存并重启 POI</strong>,使最新资源彻底生效?
3806
+ </p>
3807
+ <div
3808
+ style={{
3809
+ fontSize: '12px',
3810
+ color: '#d9822b',
3811
+ background: 'rgba(217, 130, 43, 0.1)',
3812
+ padding: '8px 10px',
3813
+ borderRadius: '4px',
3814
+ borderLeft: '3px solid #d9822b',
3815
+ }}
3816
+ >
3817
+ <span>💡 <strong>安全建议:</strong>若当前舰队正在出击战斗中,建议点击「稍后手动重启」,待出击结束回港安全后再行重启。</span>
3818
+ </div>
3819
+ </div>
3820
+ </Alert>
3425
3821
  </div>
3426
3822
  )
3427
3823
  }