haltija 1.1.21 → 1.2.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/apps/desktop/index.html +1 -1
- package/apps/desktop/main.js +264 -64
- package/apps/desktop/package.json +11 -3
- package/apps/desktop/preload.js +17 -0
- package/apps/desktop/renderer/agent-status.js +210 -0
- package/apps/desktop/renderer/settings.js +55 -0
- package/apps/desktop/renderer/state.js +98 -0
- package/apps/desktop/renderer/status.js +38 -0
- package/apps/desktop/renderer/tabs.js +374 -0
- package/apps/desktop/renderer/ui-utils.js +180 -0
- package/apps/desktop/renderer/video-capture.js +154 -0
- package/apps/desktop/renderer/webview-events.js +225 -0
- package/apps/desktop/renderer.js +98 -1604
- package/apps/desktop/resources/component.js +265 -55
- package/apps/desktop/webview-preload.js +19 -1
- package/bin/cli-subcommand.mjs +90 -27
- package/bin/hints.json +9 -4
- package/bin/hj.mjs +61 -2
- package/bin/test-data.mjs +291 -0
- package/bin/tosijs-dev.mjs +95 -20
- package/dist/client.js +5 -1
- package/dist/component.js +265 -55
- package/dist/index.js +444 -76
- package/dist/server.js +444 -76
- package/package.json +2 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared UI utilities — notifications, floating panels, drag tracking.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function escapeHtml(str) {
|
|
6
|
+
if (!str) return ''
|
|
7
|
+
return str
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Simple toast notification */
|
|
15
|
+
export function showNotification(message, duration = 2000) {
|
|
16
|
+
const existing = document.getElementById('toast-notification')
|
|
17
|
+
if (existing) existing.remove()
|
|
18
|
+
|
|
19
|
+
const toast = document.createElement('div')
|
|
20
|
+
toast.id = 'toast-notification'
|
|
21
|
+
toast.textContent = message
|
|
22
|
+
toast.style.cssText = `
|
|
23
|
+
position: fixed;
|
|
24
|
+
bottom: 20px;
|
|
25
|
+
left: 50%;
|
|
26
|
+
transform: translateX(-50%);
|
|
27
|
+
background: var(--accent, #6366f1);
|
|
28
|
+
color: white;
|
|
29
|
+
padding: 12px 24px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
font-size: 14px;
|
|
32
|
+
z-index: 10000;
|
|
33
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
34
|
+
animation: toast-in 0.2s ease-out;
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
if (!document.getElementById('toast-styles')) {
|
|
38
|
+
const style = document.createElement('style')
|
|
39
|
+
style.id = 'toast-styles'
|
|
40
|
+
style.textContent = `
|
|
41
|
+
@keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(10px); } }
|
|
42
|
+
@keyframes toast-out { to { opacity: 0; transform: translateX(-50%) translateY(10px); } }
|
|
43
|
+
`
|
|
44
|
+
document.head.appendChild(style)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
document.body.appendChild(toast)
|
|
48
|
+
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
toast.style.animation = 'toast-out 0.2s ease-in forwards'
|
|
51
|
+
setTimeout(() => toast.remove(), 200)
|
|
52
|
+
}, duration)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Track a drag operation from mousedown/touchstart */
|
|
56
|
+
export function trackDrag(event, callback, cursor = 'move') {
|
|
57
|
+
const isTouchEvent = event.type.startsWith('touch')
|
|
58
|
+
|
|
59
|
+
if (!isTouchEvent) {
|
|
60
|
+
const origX = event.clientX
|
|
61
|
+
const origY = event.clientY
|
|
62
|
+
|
|
63
|
+
const tracker = document.createElement('div')
|
|
64
|
+
tracker.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999;cursor:' + cursor
|
|
65
|
+
document.body.appendChild(tracker)
|
|
66
|
+
|
|
67
|
+
const onMove = (e) => {
|
|
68
|
+
const dx = e.clientX - origX
|
|
69
|
+
const dy = e.clientY - origY
|
|
70
|
+
if (callback(dx, dy, e) === true) {
|
|
71
|
+
tracker.removeEventListener('mousemove', onMove)
|
|
72
|
+
tracker.removeEventListener('mouseup', onMove)
|
|
73
|
+
tracker.remove()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
tracker.addEventListener('mousemove', onMove, { passive: true })
|
|
78
|
+
tracker.addEventListener('mouseup', onMove, { passive: true })
|
|
79
|
+
} else if (event.touches) {
|
|
80
|
+
const touch = event.touches[0]
|
|
81
|
+
const touchId = touch.identifier
|
|
82
|
+
const origX = touch.clientX
|
|
83
|
+
const origY = touch.clientY
|
|
84
|
+
const target = event.target
|
|
85
|
+
|
|
86
|
+
const onTouch = (e) => {
|
|
87
|
+
const t = [...e.touches].find(t => t.identifier === touchId)
|
|
88
|
+
const dx = t ? t.clientX - origX : 0
|
|
89
|
+
const dy = t ? t.clientY - origY : 0
|
|
90
|
+
if (callback(dx, dy, e) === true || !t) {
|
|
91
|
+
target.removeEventListener('touchmove', onTouch)
|
|
92
|
+
target.removeEventListener('touchend', onTouch)
|
|
93
|
+
target.removeEventListener('touchcancel', onTouch)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
target.addEventListener('touchmove', onTouch)
|
|
98
|
+
target.addEventListener('touchend', onTouch, { passive: true })
|
|
99
|
+
target.addEventListener('touchcancel', onTouch, { passive: true })
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findHighestZ() {
|
|
104
|
+
return [...document.querySelectorAll('body *')]
|
|
105
|
+
.map(el => parseFloat(getComputedStyle(el).zIndex))
|
|
106
|
+
.filter(z => !isNaN(z))
|
|
107
|
+
.reduce((max, z) => Math.max(max, z), 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Create a floating panel positioned near a target element */
|
|
111
|
+
export function createFloatPanel({ target, content, title = '', position = 's', onClose }) {
|
|
112
|
+
const existing = document.querySelector(`.float-panel[data-title="${title}"]`)
|
|
113
|
+
if (existing) {
|
|
114
|
+
existing.remove()
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const panel = document.createElement('div')
|
|
119
|
+
panel.className = 'float-panel'
|
|
120
|
+
panel.dataset.title = title
|
|
121
|
+
panel.style.zIndex = findHighestZ() + 1
|
|
122
|
+
|
|
123
|
+
panel.innerHTML = `
|
|
124
|
+
<div class="float-header">
|
|
125
|
+
<span class="float-title">${escapeHtml(title)}</span>
|
|
126
|
+
<button class="float-close" title="Close">×</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="float-content"></div>
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
panel.querySelector('.float-content').appendChild(content)
|
|
132
|
+
|
|
133
|
+
const header = panel.querySelector('.float-header')
|
|
134
|
+
header.addEventListener('mousedown', (e) => {
|
|
135
|
+
if (e.target.closest('.float-close')) return
|
|
136
|
+
panel.style.zIndex = findHighestZ() + 1
|
|
137
|
+
const x = panel.offsetLeft
|
|
138
|
+
const y = panel.offsetTop
|
|
139
|
+
trackDrag(e, (dx, dy, evt) => {
|
|
140
|
+
panel.style.left = `${x + dx}px`
|
|
141
|
+
panel.style.top = `${y + dy}px`
|
|
142
|
+
panel.style.right = 'auto'
|
|
143
|
+
panel.style.bottom = 'auto'
|
|
144
|
+
return evt.type === 'mouseup'
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
panel.querySelector('.float-close').addEventListener('click', () => {
|
|
149
|
+
panel.remove()
|
|
150
|
+
onClose?.()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
document.body.appendChild(panel)
|
|
154
|
+
|
|
155
|
+
if (target) {
|
|
156
|
+
const rect = target.getBoundingClientRect()
|
|
157
|
+
const panelRect = panel.getBoundingClientRect()
|
|
158
|
+
|
|
159
|
+
let left, top
|
|
160
|
+
switch (position) {
|
|
161
|
+
case 'n':
|
|
162
|
+
left = rect.left + rect.width / 2 - panelRect.width / 2
|
|
163
|
+
top = rect.top - panelRect.height - 8
|
|
164
|
+
break
|
|
165
|
+
case 's':
|
|
166
|
+
default:
|
|
167
|
+
left = rect.left + rect.width / 2 - panelRect.width / 2
|
|
168
|
+
top = rect.bottom + 8
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
left = Math.max(8, Math.min(left, window.innerWidth - panelRect.width - 8))
|
|
173
|
+
top = Math.max(8, Math.min(top, window.innerHeight - panelRect.height - 8))
|
|
174
|
+
|
|
175
|
+
panel.style.left = `${left}px`
|
|
176
|
+
panel.style.top = `${top}px`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return panel
|
|
180
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video capture — streams WebM chunks directly to disk via IPC.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Renderer gets media source ID from main (via webContents.getMediaSourceId)
|
|
6
|
+
* 2. Renderer opens getUserMedia stream and starts MediaRecorder
|
|
7
|
+
* 3. Each MediaRecorder chunk is sent to main as ArrayBuffer via IPC
|
|
8
|
+
* 4. Main process appends chunks to a file on disk
|
|
9
|
+
* 5. On stop, main closes the file and returns the path
|
|
10
|
+
*
|
|
11
|
+
* No base64 encoding. No large data through WebSocket.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let videoRecorder = null
|
|
15
|
+
let videoRecordingId = null
|
|
16
|
+
let videoStartTime = 0
|
|
17
|
+
let videoMaxDurationTimer = null
|
|
18
|
+
let videoStream = null
|
|
19
|
+
|
|
20
|
+
export function initVideoCapture() {
|
|
21
|
+
if (!window.haltija) return
|
|
22
|
+
|
|
23
|
+
window.haltija.onVideoStart?.(async (data) => {
|
|
24
|
+
if (videoRecorder) {
|
|
25
|
+
window.haltija.videoStartResult({ success: false, error: 'Video recording already in progress' })
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { getActiveWebview } = window._tabs
|
|
30
|
+
const webview = getActiveWebview()
|
|
31
|
+
if (!webview || !webview.getWebContentsId) {
|
|
32
|
+
window.haltija.videoStartResult({ success: false, error: 'No active webview for video capture' })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const maxDuration = (data.maxDuration || 60) * 1000
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const wcId = webview.getWebContentsId()
|
|
40
|
+
const sourceId = await window.haltija.getMediaSourceId(wcId)
|
|
41
|
+
if (!sourceId) {
|
|
42
|
+
window.haltija.videoStartResult({ success: false, error: 'Failed to get media source ID for webview' })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ask main process to create the output file
|
|
47
|
+
const fileResult = await window.haltija.videoFileCreate()
|
|
48
|
+
if (!fileResult?.success) {
|
|
49
|
+
window.haltija.videoStartResult({ success: false, error: fileResult?.error || 'Failed to create video file' })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
videoRecordingId = fileResult.recordingId
|
|
54
|
+
|
|
55
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
56
|
+
audio: false,
|
|
57
|
+
video: {
|
|
58
|
+
mandatory: {
|
|
59
|
+
chromeMediaSource: 'tab',
|
|
60
|
+
chromeMediaSourceId: sourceId,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
videoStream = stream
|
|
66
|
+
videoStartTime = Date.now()
|
|
67
|
+
|
|
68
|
+
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
69
|
+
? 'video/webm;codecs=vp9'
|
|
70
|
+
: 'video/webm'
|
|
71
|
+
|
|
72
|
+
videoRecorder = new MediaRecorder(stream, { mimeType })
|
|
73
|
+
|
|
74
|
+
videoRecorder.ondataavailable = async (e) => {
|
|
75
|
+
if (e.data.size > 0) {
|
|
76
|
+
// Convert Blob to ArrayBuffer and send to main for disk write
|
|
77
|
+
const buffer = await e.data.arrayBuffer()
|
|
78
|
+
window.haltija.videoFileChunk(videoRecordingId, buffer)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
videoRecorder.onstop = () => {
|
|
83
|
+
stream.getTracks().forEach(t => t.stop())
|
|
84
|
+
videoStream = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Emit chunks every 500ms for smooth streaming to disk
|
|
88
|
+
videoRecorder.start(500)
|
|
89
|
+
|
|
90
|
+
videoMaxDurationTimer = setTimeout(() => {
|
|
91
|
+
if (videoRecorder?.state === 'recording') {
|
|
92
|
+
console.log('[Video] Auto-stopping after max duration')
|
|
93
|
+
stopRecording()
|
|
94
|
+
}
|
|
95
|
+
}, maxDuration)
|
|
96
|
+
|
|
97
|
+
window.haltija.videoStartResult({ success: true, recordingId: videoRecordingId })
|
|
98
|
+
} catch (err) {
|
|
99
|
+
videoRecordingId = null
|
|
100
|
+
window.haltija.videoStartResult({ success: false, error: `Failed to start video: ${err.message}` })
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
window.haltija.onVideoStop?.(async () => {
|
|
105
|
+
if (!videoRecorder || videoRecorder.state === 'inactive') {
|
|
106
|
+
window.haltija.videoStopResult({ success: false, error: 'No video recording in progress' })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
await stopRecording()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
window.haltija.onVideoStatus?.(() => {
|
|
113
|
+
const recording = videoRecorder?.state === 'recording'
|
|
114
|
+
window.haltija.videoStatusResult({
|
|
115
|
+
recording,
|
|
116
|
+
recordingId: recording ? videoRecordingId : undefined,
|
|
117
|
+
duration: recording ? (Date.now() - videoStartTime) / 1000 : undefined,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function stopRecording() {
|
|
123
|
+
if (videoMaxDurationTimer) {
|
|
124
|
+
clearTimeout(videoMaxDurationTimer)
|
|
125
|
+
videoMaxDurationTimer = null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const duration = (Date.now() - videoStartTime) / 1000
|
|
129
|
+
const id = videoRecordingId
|
|
130
|
+
|
|
131
|
+
// Stop the recorder — this triggers final ondataavailable + onstop
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
videoRecorder.onstop = () => {
|
|
134
|
+
if (videoStream) {
|
|
135
|
+
videoStream.getTracks().forEach(t => t.stop())
|
|
136
|
+
videoStream = null
|
|
137
|
+
}
|
|
138
|
+
resolve()
|
|
139
|
+
}
|
|
140
|
+
videoRecorder.stop()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Small delay to ensure final chunk IPC completes
|
|
144
|
+
await new Promise(r => setTimeout(r, 100))
|
|
145
|
+
|
|
146
|
+
// Tell main to close the file and get the path
|
|
147
|
+
const result = await window.haltija.videoFileClose(id, duration)
|
|
148
|
+
|
|
149
|
+
videoRecorder = null
|
|
150
|
+
videoRecordingId = null
|
|
151
|
+
videoStartTime = 0
|
|
152
|
+
|
|
153
|
+
window.haltija.videoStopResult(result)
|
|
154
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webview event setup — navigation events, title updates, keyboard shortcuts, etc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tabs, activeTabId, el, getServerUrl } from './state.js'
|
|
6
|
+
import { updateNavButtons, checkHaltija } from './status.js'
|
|
7
|
+
|
|
8
|
+
// These are set by renderer.js to avoid circular imports
|
|
9
|
+
let _navigate, _createTab, _activateTab, _closeTab
|
|
10
|
+
export function setTabFunctions({ navigate, createTab, activateTab, closeTab }) {
|
|
11
|
+
_navigate = navigate
|
|
12
|
+
_createTab = createTab
|
|
13
|
+
_activateTab = activateTab
|
|
14
|
+
_closeTab = closeTab
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function injectWidget(webview) {
|
|
18
|
+
const currentUrl = webview.getURL()
|
|
19
|
+
if (!currentUrl || currentUrl === 'about:blank') return
|
|
20
|
+
|
|
21
|
+
const serverUrl = getServerUrl()
|
|
22
|
+
const script = `
|
|
23
|
+
(function() {
|
|
24
|
+
if (document.getElementById('haltija-widget')) return;
|
|
25
|
+
fetch('${serverUrl}/inject.js')
|
|
26
|
+
.then(r => r.text())
|
|
27
|
+
.then(code => eval(code))
|
|
28
|
+
.catch(e => console.error('[Haltija] Injection failed:', e));
|
|
29
|
+
})();
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await webview.executeJavaScript(script)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('[Haltija Desktop] Injection failed:', err)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setupWebviewEvents(tab) {
|
|
40
|
+
const webview = tab.webview
|
|
41
|
+
|
|
42
|
+
webview.addEventListener('did-start-loading', () => {
|
|
43
|
+
webview.classList.add('loading')
|
|
44
|
+
if (tab.id === activeTabId) {
|
|
45
|
+
el.statusDot.className = 'status-dot connecting'
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
webview.addEventListener('did-stop-loading', () => {
|
|
50
|
+
webview.classList.remove('loading')
|
|
51
|
+
if (tab.id === activeTabId) {
|
|
52
|
+
updateNavButtons()
|
|
53
|
+
checkHaltija()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
webview.addEventListener('did-navigate', (e) => {
|
|
58
|
+
tab.url = e.url
|
|
59
|
+
if (tab.id === activeTabId && !tab.isTerminal) {
|
|
60
|
+
el.urlInput.value = e.url
|
|
61
|
+
updateNavButtons()
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
webview.addEventListener('did-navigate-in-page', (e) => {
|
|
66
|
+
tab.url = e.url
|
|
67
|
+
if (tab.id === activeTabId && !tab.isTerminal) {
|
|
68
|
+
el.urlInput.value = e.url
|
|
69
|
+
updateNavButtons()
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
webview.addEventListener('did-finish-load', () => {
|
|
74
|
+
if (window.haltija) {
|
|
75
|
+
window.haltija.webviewReady(webview.getWebContentsId())
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
webview.addEventListener('dom-ready', () => {
|
|
80
|
+
const url = webview.getURL()
|
|
81
|
+
if (url.startsWith('blob:') || url.startsWith('data:')) {
|
|
82
|
+
webview.insertCSS(':root { color-scheme: light !important; }')
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
webview.addEventListener('page-title-updated', (e) => {
|
|
87
|
+
tab.title = e.title || 'New Tab'
|
|
88
|
+
tab.element.querySelector('.tab-title').textContent = tab.title
|
|
89
|
+
if (tab.id === activeTabId) {
|
|
90
|
+
document.title = tab.title || 'Haltija'
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
webview.addEventListener('new-window', async (e) => {
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
const { settings } = await import('./state.js')
|
|
97
|
+
if (settings.confirmNewTabs) {
|
|
98
|
+
const { showNewTabDialog } = await import('./settings.js')
|
|
99
|
+
const allowed = await showNewTabDialog(e.url)
|
|
100
|
+
if (allowed) _createTab(e.url)
|
|
101
|
+
} else {
|
|
102
|
+
_createTab(e.url)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
webview.addEventListener('console-message', (e) => {
|
|
107
|
+
const prefix = e.level === 2 ? '[warn]' : e.level === 3 ? '[error]' : ''
|
|
108
|
+
console.log(`[Tab ${tab.id}${prefix}]`, e.message)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
webview.addEventListener('dialog', (e) => {
|
|
112
|
+
const dialogType = e.type
|
|
113
|
+
const message = e.messageText || ''
|
|
114
|
+
console.log(`[Tab ${tab.id}] Native dialog intercepted: ${dialogType} - "${message}"`)
|
|
115
|
+
|
|
116
|
+
if (dialogType === 'confirm') {
|
|
117
|
+
e.dialog.accept()
|
|
118
|
+
} else if (dialogType === 'prompt') {
|
|
119
|
+
e.dialog.dismiss()
|
|
120
|
+
} else {
|
|
121
|
+
e.dialog.accept()
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
webview.addEventListener('context-menu', (e) => {
|
|
126
|
+
e.preventDefault()
|
|
127
|
+
const { x, y } = e.params
|
|
128
|
+
|
|
129
|
+
const menu = document.createElement('div')
|
|
130
|
+
menu.className = 'context-menu'
|
|
131
|
+
menu.style.cssText = `
|
|
132
|
+
position: fixed;
|
|
133
|
+
left: ${e.clientX || x}px;
|
|
134
|
+
top: ${e.clientY || y}px;
|
|
135
|
+
background: var(--surface);
|
|
136
|
+
border: 1px solid var(--border);
|
|
137
|
+
border-radius: 6px;
|
|
138
|
+
padding: 4px 0;
|
|
139
|
+
min-width: 150px;
|
|
140
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
141
|
+
z-index: 10000;
|
|
142
|
+
`
|
|
143
|
+
|
|
144
|
+
const items = [
|
|
145
|
+
{ label: 'Back', action: () => webview.canGoBack() && webview.goBack(), enabled: webview.canGoBack() },
|
|
146
|
+
{ label: 'Forward', action: () => webview.canGoForward() && webview.goForward(), enabled: webview.canGoForward() },
|
|
147
|
+
{ label: 'Reload', action: () => webview.reload() },
|
|
148
|
+
{ type: 'separator' },
|
|
149
|
+
{ label: 'Inspect Element', action: () => webview.inspectElement(x, y) },
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
items.forEach((item) => {
|
|
153
|
+
if (item.type === 'separator') {
|
|
154
|
+
const sep = document.createElement('div')
|
|
155
|
+
sep.style.cssText = 'height: 1px; background: var(--border); margin: 4px 0;'
|
|
156
|
+
menu.appendChild(sep)
|
|
157
|
+
} else {
|
|
158
|
+
const menuItem = document.createElement('div')
|
|
159
|
+
menuItem.textContent = item.label
|
|
160
|
+
menuItem.style.cssText = `
|
|
161
|
+
padding: 6px 12px;
|
|
162
|
+
cursor: ${item.enabled === false ? 'default' : 'pointer'};
|
|
163
|
+
opacity: ${item.enabled === false ? '0.5' : '1'};
|
|
164
|
+
color: var(--text);
|
|
165
|
+
`
|
|
166
|
+
if (item.enabled !== false) {
|
|
167
|
+
menuItem.addEventListener('mouseenter', () => { menuItem.style.background = 'var(--hover)' })
|
|
168
|
+
menuItem.addEventListener('mouseleave', () => { menuItem.style.background = 'transparent' })
|
|
169
|
+
menuItem.addEventListener('click', () => {
|
|
170
|
+
document.body.removeChild(menu)
|
|
171
|
+
item.action()
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
menu.appendChild(menuItem)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
document.body.appendChild(menu)
|
|
179
|
+
|
|
180
|
+
const closeMenu = (evt) => {
|
|
181
|
+
if (!menu.contains(evt.target)) {
|
|
182
|
+
if (document.body.contains(menu)) document.body.removeChild(menu)
|
|
183
|
+
document.removeEventListener('click', closeMenu)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
setTimeout(() => document.addEventListener('click', closeMenu), 0)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
webview.addEventListener('before-input-event', (e) => {
|
|
190
|
+
const input = e.input || e
|
|
191
|
+
const { type, key, meta, control } = input
|
|
192
|
+
if (type !== 'keyDown') return
|
|
193
|
+
|
|
194
|
+
if ((meta || control) && key === 'r') {
|
|
195
|
+
e.preventDefault(); e.stopPropagation()
|
|
196
|
+
webview.reload()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
if ((meta || control) && key === 'l') {
|
|
200
|
+
e.preventDefault(); e.stopPropagation()
|
|
201
|
+
el.urlInput.focus(); el.urlInput.select()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if ((meta || control) && key === 't') {
|
|
205
|
+
e.preventDefault(); e.stopPropagation()
|
|
206
|
+
_createTab()
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
if ((meta || control) && key === 'w') {
|
|
210
|
+
e.preventDefault(); e.stopPropagation()
|
|
211
|
+
_closeTab(tab.id)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if ((meta || control) && key === '[') {
|
|
215
|
+
e.preventDefault(); e.stopPropagation()
|
|
216
|
+
if (webview.canGoBack()) webview.goBack()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if ((meta || control) && key === ']') {
|
|
220
|
+
e.preventDefault(); e.stopPropagation()
|
|
221
|
+
if (webview.canGoForward()) webview.goForward()
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
}
|