Skip to content

Commit d41c6b3

Browse files
authored
Merge pull request #11 from GwIhViEte/修复被某些考试软件抢占窗口的问题
fix: 修复被某些考试软件抢占窗口的问题
2 parents 4a0bbb4 + 4823ff1 commit d41c6b3

File tree

7 files changed

+135
-24
lines changed

7 files changed

+135
-24
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "interview-coder-cn",
3-
"version": "1.4.0",
3+
"version": "1.4.2",
44
"description": "编码面试解题助手,实时截屏并生成解题思路和答案",
55
"main": "./out/main/index.js",
66
"scripts": {

src/main/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import 'dotenv/config'
22
import { app, BrowserWindow, globalShortcut, dialog } from 'electron'
33
import { autoUpdater } from 'electron-updater'
44

5+
type AbortLikeError = {
6+
name?: string
7+
code?: string
8+
message?: unknown
9+
}
10+
511
// Swallow AbortError from user-initiated stream cancellations to keep console clean
612
function isAbortError(error: unknown): boolean {
7-
const err = error as any
8-
return (
9-
!!err &&
10-
(err.name === 'AbortError' || err.code === 'ABORT_ERR' || /aborted/i.test(String(err.message)))
11-
)
13+
if (typeof error !== 'object' || error === null) {
14+
return false
15+
}
16+
const err = error as AbortLikeError
17+
const message = typeof err.message === 'string' ? err.message : ''
18+
return err.name === 'AbortError' || err.code === 'ABORT_ERR' || /aborted/i.test(message)
1219
}
1320

1421
process.on('unhandledRejection', (error) => {

src/main/shortcuts.ts

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { globalShortcut, ipcMain } from 'electron'
2+
import type { BrowserWindow } from 'electron'
23
import type { ModelMessage } from 'ai'
34
import { takeScreenshot } from './take-screenshot'
45
import { getSolutionStream, getFollowUpStream } from './ai'
@@ -9,6 +10,7 @@ type Shortcut = {
910
action: string
1011
key: string
1112
status: ShortcutStatus
13+
registeredKeys: string[]
1214
}
1315

1416
enum ShortcutStatus {
@@ -33,6 +35,44 @@ let currentStreamContext: StreamContext | null = null
3335
// Conversation history tracking
3436
let conversationMessages: ModelMessage[] = []
3537

38+
const FRONT_REASSERT_DURATION = 5000
39+
const FRONT_REASSERT_INTERVAL = 150
40+
const FRONT_RELATIVE_LEVEL = 10
41+
let frontReassertTimer: NodeJS.Timeout | null = null
42+
43+
function applyTopMost(win: BrowserWindow) {
44+
if (!win || win.isDestroyed()) return
45+
win.setAlwaysOnTop(true, 'screen-saver', FRONT_RELATIVE_LEVEL)
46+
win.moveTop()
47+
}
48+
49+
function keepWindowInFront(window: BrowserWindow) {
50+
if (!window || window.isDestroyed()) return
51+
if (frontReassertTimer) {
52+
clearInterval(frontReassertTimer)
53+
frontReassertTimer = null
54+
}
55+
56+
const start = Date.now()
57+
const reassert = () => {
58+
if (!window.isVisible() || window.isDestroyed()) return false
59+
applyTopMost(window)
60+
return true
61+
}
62+
63+
if (!reassert()) return
64+
65+
frontReassertTimer = setInterval(() => {
66+
const shouldStop = Date.now() - start > FRONT_REASSERT_DURATION
67+
if (shouldStop || !reassert()) {
68+
if (frontReassertTimer) {
69+
clearInterval(frontReassertTimer)
70+
frontReassertTimer = null
71+
}
72+
}
73+
}, FRONT_REASSERT_INTERVAL)
74+
}
75+
3676
function abortCurrentStream(reason: AbortReason) {
3777
if (!currentStreamContext) return
3878
currentStreamContext.reason = reason
@@ -46,7 +86,13 @@ const callbacks: Record<string, () => void> = {
4686
if (mainWindow.isVisible()) {
4787
mainWindow.hide()
4888
} else {
49-
mainWindow.show()
89+
// 重新显示时不断重申置顶属性,抵消其他前台软件持续抢占
90+
if (process.platform === 'darwin' || process.platform === 'win32') {
91+
mainWindow.showInactive()
92+
} else {
93+
mainWindow.show()
94+
}
95+
keepWindowInFront(mainWindow)
5096
}
5197
},
5298

@@ -194,16 +240,60 @@ const callbacks: Record<string, () => void> = {
194240
}
195241
}
196242

243+
function unregisterShortcut(action: string) {
244+
const shortcut = shortcuts[action]
245+
if (!shortcut) return
246+
if (shortcut.registeredKeys.length) {
247+
shortcut.registeredKeys.forEach((registeredKey) => {
248+
globalShortcut.unregister(registeredKey)
249+
})
250+
} else {
251+
globalShortcut.unregister(shortcut.key)
252+
}
253+
shortcut.status = ShortcutStatus.Available
254+
shortcut.registeredKeys = []
255+
}
256+
257+
function getShortcutRegistrationKeys(key: string) {
258+
const keys = [key]
259+
if (process.platform !== 'win32') {
260+
return keys
261+
}
262+
const parts = key.split('+')
263+
const hasAlt = parts.includes('Alt')
264+
const hasCtrl = parts.includes('CommandOrControl') || parts.includes('Control')
265+
if (hasAlt && !hasCtrl) {
266+
const aliasParts = [...parts]
267+
const altIndex = aliasParts.indexOf('Alt')
268+
if (altIndex >= 0) {
269+
aliasParts.splice(altIndex, 0, 'CommandOrControl')
270+
const aliasKey = aliasParts.join('+')
271+
if (!keys.includes(aliasKey)) {
272+
keys.push(aliasKey)
273+
}
274+
}
275+
}
276+
return keys
277+
}
278+
197279
function registerShortcut(action: string, key: string) {
198-
if (shortcuts[action]?.status === ShortcutStatus.Registered) {
199-
globalShortcut.unregister(shortcuts[action].key)
200-
shortcuts[action].status = ShortcutStatus.Available
280+
if (shortcuts[action]) {
281+
unregisterShortcut(action)
201282
}
202-
const ok = globalShortcut.register(key, callbacks[action])
283+
284+
const keysToRegister = getShortcutRegistrationKeys(key)
285+
const registeredKeys: string[] = []
286+
keysToRegister.forEach((shortcutKey) => {
287+
if (globalShortcut.register(shortcutKey, callbacks[action])) {
288+
registeredKeys.push(shortcutKey)
289+
}
290+
})
291+
203292
shortcuts[action] = {
204293
action,
205294
key,
206-
status: ok ? ShortcutStatus.Registered : ShortcutStatus.Failed
295+
status: registeredKeys.length ? ShortcutStatus.Registered : ShortcutStatus.Failed,
296+
registeredKeys
207297
}
208298
}
209299

src/renderer/src/coder/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function CoderPage() {
3232
return () => {
3333
window.api.removeSyncAppStateListener()
3434
}
35-
}, [])
35+
}, [syncAppState])
3636

3737
return (
3838
<>

src/renderer/src/lib/store/shortcuts.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ interface ShortcutsStore extends ShortcutsState {
2626
resetShortcuts: () => void
2727
}
2828

29+
type PersistedShortcutsState = {
30+
shortcuts?: Record<string, Shortcut>
31+
}
32+
33+
function isPersistedShortcutsState(value: unknown): value is PersistedShortcutsState {
34+
return typeof value === 'object' && value !== null && 'shortcuts' in value
35+
}
36+
2937
const defaultShortcuts: Record<string, Omit<Shortcut, 'defaultKey'>> = {
3038
hideOrShowMainWindow: {
3139
action: 'hideOrShowMainWindow',
@@ -101,8 +109,8 @@ export const useShortcutsStore = create<ShortcutsStore>()(
101109
{
102110
name: 'interview-coder-shortcuts',
103111
version: 2,
104-
migrate: (state: any) => {
105-
if (!state?.shortcuts) return state
112+
migrate: (state: unknown) => {
113+
if (!isPersistedShortcutsState(state) || !state.shortcuts) return state as ShortcutsStore
106114
// Merge in any new default shortcuts that are missing
107115
const defaults = Object.fromEntries(
108116
Object.entries(defaultShortcuts).map(([action, shortcut]) => [
@@ -116,7 +124,7 @@ export const useShortcutsStore = create<ShortcutsStore>()(
116124
...defaults,
117125
...state.shortcuts
118126
}
119-
}
127+
} as ShortcutsStore
120128
}
121129
}
122130
)

src/renderer/src/lib/utils/keyboard.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,14 @@ export function getShortcutAccelerator(event: KeyboardEvent) {
100100
}
101101

102102
const modifiers: string[] = []
103-
if (event.ctrlKey) modifiers.push(isMac ? 'Control' : 'CommandOrControl')
104-
if (event.altKey) modifiers.push('Alt')
103+
// AltRight on Windows reports AltGraph and toggles ctrlKey, so treat it as plain Alt
104+
const isAltGraph =
105+
typeof event.getModifierState === 'function' && event.getModifierState('AltGraph')
106+
const isCtrlActive = event.ctrlKey && !isAltGraph
107+
const isAltActive = event.altKey || isAltGraph
108+
109+
if (isCtrlActive) modifiers.push(isMac ? 'Control' : 'CommandOrControl')
110+
if (isAltActive) modifiers.push('Alt')
105111
if (event.shiftKey) modifiers.push('Shift')
106112
if (event.metaKey) modifiers.push(isMac ? 'CommandOrControl' : 'Meta')
107113
if (modifiers.length === 0) return null

0 commit comments

Comments
 (0)