@owear/cli 0.1.2 → 0.1.4
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/package.json +10 -8
- package/src/ow.js +548 -25
- package/template/README.md +34 -28
- package/template/app/main.ts +273 -4
- package/template/index.html +117 -28
- package/template/package.json +1 -1
- package/template/public/favicon.svg +4 -0
- package/template/src/assets/dancing-script.woff2 +0 -0
- package/template/src/assets/jetbrains-mono.woff2 +0 -0
- package/template/src/ow.d.ts +9 -1
- package/template/src/renderer.ts +489 -1
- package/template/src/style.css +280 -56
- package/template-installer/README.md +38 -0
- package/template-installer/app/main.ts +22 -0
- package/template-installer/index.html +70 -0
- package/template-installer/package.json +22 -0
- package/template-installer/src/assets/dancing-script.woff2 +0 -0
- package/template-installer/src/assets/jetbrains-mono.woff2 +0 -0
- package/template-installer/src/installer.ts +171 -0
- package/template-installer/src/style.css +442 -0
- package/template-installer/tsconfig.json +13 -0
- package/template-installer/uninstaller/app/main.ts +16 -0
- package/template-installer/uninstaller/index.html +54 -0
- package/template-installer/uninstaller/package.json +22 -0
- package/template-installer/uninstaller/src/uninstaller.ts +101 -0
- package/template-installer/uninstaller/vite.config.ts +10 -0
- package/template-installer/vite.config.ts +15 -0
- package/tools/owear-installer.mjs +86 -0
- package/tools/owear-linux.mjs +158 -0
- package/tools/owear-msi.mjs +113 -0
- package/tools/owear-pack.mjs +93 -0
- package/tools/owear-sign.mjs +204 -0
- package/tools/owear-update.mjs +150 -0
- package/LICENSE +0 -201
package/template/src/renderer.ts
CHANGED
|
@@ -45,6 +45,30 @@ function renderEnvironment(): void {
|
|
|
45
45
|
$('#env-ua').textContent = ua
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// ── App (C1): rutas/identidad vía el puente Node + eventos de ciclo de vida ─
|
|
49
|
+
async function renderApp(): Promise<void> {
|
|
50
|
+
try {
|
|
51
|
+
const info = await invoke<Record<string, unknown>>('node', 'call', {
|
|
52
|
+
fn: 'starter.appInfo',
|
|
53
|
+
args: [],
|
|
54
|
+
})
|
|
55
|
+
$('#app-name').textContent = String(info.name ?? '—')
|
|
56
|
+
$('#app-version').textContent = String(info.version ?? '—')
|
|
57
|
+
$('#app-packaged').textContent = info.packaged ? 'sí' : 'no'
|
|
58
|
+
|
|
59
|
+
const paths = await invoke<Record<string, string>>('node', 'call', {
|
|
60
|
+
fn: 'starter.appPaths',
|
|
61
|
+
args: [],
|
|
62
|
+
})
|
|
63
|
+
for (const name of ['userData', 'exe', 'appPath']) {
|
|
64
|
+
const el = document.getElementById(`app-path-${name}`)
|
|
65
|
+
if (el) el.textContent = paths[name] ?? '—'
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
log(`app.* no disponible: ${e}`, 'err')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
48
72
|
// ── Acciones (módulos nativos) ──────────────────────────────────────────
|
|
49
73
|
async function openFile(): Promise<void> {
|
|
50
74
|
const path = await invoke<string | null>('dialog', 'open', 'open', 'Abrir un archivo')
|
|
@@ -99,7 +123,315 @@ async function about(): Promise<void> {
|
|
|
99
123
|
log('dialog.messageBox → cerrado', 'ok')
|
|
100
124
|
}
|
|
101
125
|
|
|
126
|
+
// ── C2: diálogos completos (showOpenDialog/showSaveDialog/showMessageBox) ──
|
|
127
|
+
async function openMultiple(): Promise<void> {
|
|
128
|
+
const r = await invoke<{ canceled: boolean; filePaths: string[] }>(
|
|
129
|
+
'dialog',
|
|
130
|
+
'showOpenDialog',
|
|
131
|
+
{
|
|
132
|
+
title: 'Abrir varios',
|
|
133
|
+
properties: ['openFile', 'multiSelections'],
|
|
134
|
+
filters: [{ name: 'Texto', extensions: ['txt', 'md', 'json'] }],
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
if (r.canceled) return log('showOpenDialog → cancelado')
|
|
138
|
+
log(`showOpenDialog → ${r.filePaths.length} archivo(s)`, 'ok')
|
|
139
|
+
for (const p of r.filePaths.slice(0, 4)) log(` · ${p}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function saveAs(): Promise<void> {
|
|
143
|
+
const r = await invoke<{ canceled: boolean; filePath: string }>('dialog', 'showSaveDialog', {
|
|
144
|
+
title: 'Guardar como',
|
|
145
|
+
defaultPath: `owear-${Date.now()}.txt`,
|
|
146
|
+
filters: [{ name: 'Texto', extensions: ['txt', 'md'] }],
|
|
147
|
+
})
|
|
148
|
+
if (r.canceled) return log('showSaveDialog → cancelado')
|
|
149
|
+
lastPath = r.filePath
|
|
150
|
+
log(`showSaveDialog → ${r.filePath}`, 'ok')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── C3: webContents ──────────────────────────────────────────────────────
|
|
154
|
+
async function capturePage(): Promise<void> {
|
|
155
|
+
const r = await invoke<{ data: string }>('node', 'call', {
|
|
156
|
+
fn: 'starter.capture',
|
|
157
|
+
args: [],
|
|
158
|
+
})
|
|
159
|
+
log(`webContents.capturePage → ${Math.round((r.data.length * 3) / 4)} bytes PNG`, 'ok')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── C4: nativeImage ──────────────────────────────────────────────────────
|
|
163
|
+
async function nativeImageTest(): Promise<void> {
|
|
164
|
+
const r = await invoke<{
|
|
165
|
+
original: { width: number; height: number }
|
|
166
|
+
resized: { width: number; height: number }
|
|
167
|
+
cropped: { width: number; height: number }
|
|
168
|
+
pngBytes: number
|
|
169
|
+
fromDataURL: { width: number; height: number }
|
|
170
|
+
}>('node', 'call', { fn: 'starter.nativeImage', args: [] })
|
|
171
|
+
log(
|
|
172
|
+
`nativeImage → ${r.original.width}×${r.original.height} · resize ${r.resized.width}×${r.resized.height} · crop ${r.cropped.width}×${r.cropped.height} · ${r.pngBytes} B · dataURL ${r.fromDataURL.width}×${r.fromDataURL.height}`,
|
|
173
|
+
'ok',
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── C5: menú contextual ──────────────────────────────────────────────────
|
|
178
|
+
async function showMenu(): Promise<void> {
|
|
179
|
+
await invoke('node', 'call', { fn: 'starter.menu', args: [] })
|
|
180
|
+
log('menu.popup → abierto (elige una opción)', 'ok')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── C6: tray ─────────────────────────────────────────────────────────────
|
|
184
|
+
async function toggleTray(): Promise<void> {
|
|
185
|
+
const r = await invoke<{ created: boolean; id: string }>('node', 'call', {
|
|
186
|
+
fn: 'starter.tray',
|
|
187
|
+
args: [],
|
|
188
|
+
})
|
|
189
|
+
log(r.created ? `tray creado (${r.id})` : `tray ya existe (${r.id})`, 'ok')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── C7: nativeTheme ──────────────────────────────────────────────────────
|
|
193
|
+
function applyTheme(dark: boolean): void {
|
|
194
|
+
document.documentElement.dataset.theme = dark ? 'dark' : 'light'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function loadTheme(): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const info = await invoke<{ dark: boolean; source: string }>('node', 'call', {
|
|
200
|
+
fn: 'starter.themeGet',
|
|
201
|
+
args: [],
|
|
202
|
+
})
|
|
203
|
+
applyTheme(!!info.dark)
|
|
204
|
+
log(`theme.get → dark=${info.dark} source=${info.source}`, 'muted')
|
|
205
|
+
} catch (e) {
|
|
206
|
+
logError(e)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function setTheme(source: string): Promise<void> {
|
|
211
|
+
const info = await invoke<{ dark: boolean; source: string }>('node', 'call', {
|
|
212
|
+
fn: 'starter.themeSet',
|
|
213
|
+
args: [source],
|
|
214
|
+
})
|
|
215
|
+
applyTheme(!!info.dark)
|
|
216
|
+
log(`theme.setSource('${source}') → dark=${info.dark}`, 'ok')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── C8: print / printToPDF ───────────────────────────────────────────────
|
|
220
|
+
async function exportPdf(): Promise<void> {
|
|
221
|
+
const r = await invoke<{ ok: boolean; bytes?: number; path?: string; error?: string }>(
|
|
222
|
+
'node',
|
|
223
|
+
'call',
|
|
224
|
+
{ fn: 'starter.printPdf', args: [] },
|
|
225
|
+
)
|
|
226
|
+
if (r.ok) log(`printToPDF → ${r.bytes} bytes → ${r.path}`, 'ok')
|
|
227
|
+
else log(`printToPDF no disponible: ${r.error}`, 'err')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function printDoc(): Promise<void> {
|
|
231
|
+
await invoke('node', 'call', { fn: 'starter.print', args: [] })
|
|
232
|
+
log('print → diálogo de impresión abierto', 'ok')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── C9: BrowserWindow completo ───────────────────────────────────────────
|
|
236
|
+
async function childWindow(): Promise<void> {
|
|
237
|
+
const r = await invoke<{ id: number }>('node', 'call', {
|
|
238
|
+
fn: 'starter.childWindow',
|
|
239
|
+
args: [],
|
|
240
|
+
})
|
|
241
|
+
log(`ventana secundaria → #${r.id} (parent + backgroundColor)`, 'ok')
|
|
242
|
+
}
|
|
243
|
+
async function toggleOnTop(): Promise<void> {
|
|
244
|
+
const r = await invoke<{ on: boolean }>('node', 'call', {
|
|
245
|
+
fn: 'starter.toggleOnTop',
|
|
246
|
+
args: [],
|
|
247
|
+
})
|
|
248
|
+
log(`setAlwaysOnTop(${r.on})`, 'ok')
|
|
249
|
+
}
|
|
250
|
+
async function progressBar(): Promise<void> {
|
|
251
|
+
await invoke('node', 'call', { fn: 'starter.progress', args: [] })
|
|
252
|
+
log('setProgressBar(0.5) → barra de tareas', 'ok')
|
|
253
|
+
}
|
|
254
|
+
async function ignoreMouse(): Promise<void> {
|
|
255
|
+
await invoke('node', 'call', { fn: 'starter.ignoreMouse', args: [] })
|
|
256
|
+
log('setIgnoreMouseEvents(true) 3 s…', 'muted')
|
|
257
|
+
}
|
|
258
|
+
async function listWindows(): Promise<void> {
|
|
259
|
+
const r = await invoke<{ focused: number | null; count: number; ids: number[] }>(
|
|
260
|
+
'node',
|
|
261
|
+
'call',
|
|
262
|
+
{ fn: 'starter.windows', args: [] },
|
|
263
|
+
)
|
|
264
|
+
log(`windows → foco=#${r.focused} total=${r.count} ids=[${r.ids}]`, 'ok')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── C10: pantallas (multi-monitor) y energía ─────────────────────────────
|
|
268
|
+
interface DisplayRect {
|
|
269
|
+
x: number
|
|
270
|
+
y: number
|
|
271
|
+
width: number
|
|
272
|
+
height: number
|
|
273
|
+
}
|
|
274
|
+
interface Display {
|
|
275
|
+
id: number
|
|
276
|
+
primary: boolean
|
|
277
|
+
label: string
|
|
278
|
+
bounds: DisplayRect
|
|
279
|
+
size: { width: number; height: number }
|
|
280
|
+
workArea: DisplayRect
|
|
281
|
+
workAreaSize: { width: number; height: number }
|
|
282
|
+
scaleFactor: number
|
|
283
|
+
rotation: number
|
|
284
|
+
internal: boolean
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderMonitorMap(displays: Display[], cursor: { x: number; y: number }): void {
|
|
288
|
+
const map = $('#mon-map')
|
|
289
|
+
map.textContent = ''
|
|
290
|
+
if (!displays.length) {
|
|
291
|
+
map.innerHTML = '<span class="monmap__hint">Sin monitores.</span>'
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
const minX = Math.min(...displays.map((d) => d.bounds.x))
|
|
295
|
+
const minY = Math.min(...displays.map((d) => d.bounds.y))
|
|
296
|
+
const maxX = Math.max(...displays.map((d) => d.bounds.x + d.bounds.width))
|
|
297
|
+
const maxY = Math.max(...displays.map((d) => d.bounds.y + d.bounds.height))
|
|
298
|
+
const w = maxX - minX || 1
|
|
299
|
+
const h = maxY - minY || 1
|
|
300
|
+
const pad = 6
|
|
301
|
+
const span = 100 - pad * 2
|
|
302
|
+
const sx = (px: number): number => pad + ((px - minX) / w) * span
|
|
303
|
+
const sy = (py: number): number => pad + ((py - minY) / h) * span
|
|
304
|
+
|
|
305
|
+
for (const d of displays) {
|
|
306
|
+
const cell = document.createElement('div')
|
|
307
|
+
cell.className = `monmap__cell${d.primary ? ' monmap__cell--primary' : ''}`
|
|
308
|
+
cell.style.left = `${sx(d.bounds.x)}%`
|
|
309
|
+
cell.style.top = `${sy(d.bounds.y)}%`
|
|
310
|
+
cell.style.width = `${(d.bounds.width / w) * span}%`
|
|
311
|
+
cell.style.height = `${(d.bounds.height / h) * span}%`
|
|
312
|
+
const rot = d.rotation ? ` · ${d.rotation}°` : ''
|
|
313
|
+
cell.innerHTML =
|
|
314
|
+
`<span class="monmap__id">#${d.id}${d.primary ? ' ★' : ''}</span>` +
|
|
315
|
+
`<span class="monmap__meta">${d.size.width}×${d.size.height} · ${d.scaleFactor}×${rot}</span>`
|
|
316
|
+
map.appendChild(cell)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const cur = document.createElement('span')
|
|
320
|
+
cur.className = 'monmap__cursor'
|
|
321
|
+
cur.style.left = `${sx(cursor.x)}%`
|
|
322
|
+
cur.style.top = `${sy(cursor.y)}%`
|
|
323
|
+
map.appendChild(cur)
|
|
324
|
+
|
|
325
|
+
$('#mon-count').textContent = `${displays.length} monitor(es)`
|
|
326
|
+
const p = displays.find((d) => d.primary)
|
|
327
|
+
$('#mon-primary').textContent = p ? `primario #${p.id}` : '—'
|
|
328
|
+
$('#mon-list').innerHTML = displays
|
|
329
|
+
.map(
|
|
330
|
+
(d) =>
|
|
331
|
+
`<div class="monlist__row"><code>#${d.id}</code> ${d.label ? `${d.label} · ` : ''}` +
|
|
332
|
+
`${d.bounds.x},${d.bounds.y} · ${d.bounds.width}×${d.bounds.height}` +
|
|
333
|
+
` · work ${d.workArea.width}×${d.workArea.height} · ${d.scaleFactor}×` +
|
|
334
|
+
`${d.internal ? ' · interno' : ''}</div>`,
|
|
335
|
+
)
|
|
336
|
+
.join('')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function showScreens(): Promise<void> {
|
|
340
|
+
const r = await invoke<{
|
|
341
|
+
displays: Display[]
|
|
342
|
+
primaryId: number | null
|
|
343
|
+
cursor: { x: number; y: number }
|
|
344
|
+
}>('node', 'call', { fn: 'starter.screens', args: [] })
|
|
345
|
+
renderMonitorMap(r.displays, r.cursor)
|
|
346
|
+
log(`screen → ${r.displays.length} monitor(es), primario #${r.primaryId}`, 'ok')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function nearestDisplay(): Promise<void> {
|
|
350
|
+
const r = await invoke<{ cursor: { x: number; y: number }; display: Display | null }>(
|
|
351
|
+
'node',
|
|
352
|
+
'call',
|
|
353
|
+
{ fn: 'starter.nearest', args: [] },
|
|
354
|
+
)
|
|
355
|
+
log(
|
|
356
|
+
`screen.nearest → cursor ${r.cursor.x},${r.cursor.y} ⇒ display #${r.display?.id ?? '—'}`,
|
|
357
|
+
'ok',
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let powerLogReady = false
|
|
362
|
+
function appendPower(ev: string): void {
|
|
363
|
+
const el = $('#power-log')
|
|
364
|
+
if (!powerLogReady) {
|
|
365
|
+
el.textContent = ''
|
|
366
|
+
powerLogReady = true
|
|
367
|
+
}
|
|
368
|
+
el.textContent += `[${new Date().toLocaleTimeString()}] ${ev}\n`
|
|
369
|
+
el.scrollTop = el.scrollHeight
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function powerOn(): Promise<void> {
|
|
373
|
+
const r = await invoke<{ onBattery: boolean; idle: number }>('node', 'call', {
|
|
374
|
+
fn: 'starter.powerOn',
|
|
375
|
+
args: [],
|
|
376
|
+
})
|
|
377
|
+
$('#power-status').textContent = r.onBattery ? 'batería' : 'AC'
|
|
378
|
+
appendPower(`powerMonitor ON · onBattery=${r.onBattery} idle=${r.idle}s`)
|
|
379
|
+
log(`powerMonitor → onBattery=${r.onBattery} idle=${r.idle}s`, 'ok')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function toggleBlocker(): Promise<void> {
|
|
383
|
+
const r = await invoke<{ on: boolean; id?: number }>('node', 'call', {
|
|
384
|
+
fn: 'starter.blocker',
|
|
385
|
+
args: [],
|
|
386
|
+
})
|
|
387
|
+
$('#power-status').textContent = r.on ? 'pantalla activa' : 'bloqueador OFF'
|
|
388
|
+
log(`powerSaveBlocker → ${r.on ? `ON (id ${r.id})` : 'OFF'}`, 'ok')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function idleInfo(): Promise<void> {
|
|
392
|
+
const r = await invoke<{ idleTime: number; state: string }>('node', 'call', {
|
|
393
|
+
fn: 'starter.idle',
|
|
394
|
+
args: [],
|
|
395
|
+
})
|
|
396
|
+
log(`powerMonitor.idle → ${r.idleTime}s · estado «${r.state}»`, 'ok')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function askDialog(): Promise<void> {
|
|
400
|
+
const r = await invoke<{ response: number; checkboxChecked: boolean }>(
|
|
401
|
+
'dialog',
|
|
402
|
+
'showMessageBox',
|
|
403
|
+
{
|
|
404
|
+
type: 'question',
|
|
405
|
+
title: 'C2',
|
|
406
|
+
message: '¿Continuar?',
|
|
407
|
+
detail: 'showMessageBox con botones y checkbox.',
|
|
408
|
+
buttons: ['Sí', 'No', 'Cancelar'],
|
|
409
|
+
defaultId: 0,
|
|
410
|
+
cancelId: 2,
|
|
411
|
+
checkboxLabel: 'No volver a preguntar',
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
log(`showMessageBox → response=${r.response} checkbox=${r.checkboxChecked}`, 'ok')
|
|
415
|
+
}
|
|
416
|
+
|
|
102
417
|
// ── Titlebar (builtin ow-window) ────────────────────────────────────────
|
|
418
|
+
/**
|
|
419
|
+
* Si el SO provee botones de ventana nativos (titleBarOverlay), ocultamos los
|
|
420
|
+
* del web y reservamos su hueco. La clase y las CSS vars las consume style.css.
|
|
421
|
+
*/
|
|
422
|
+
function applyTitleBarOverlay(): void {
|
|
423
|
+
const ov = window.__owTitlebarOverlay
|
|
424
|
+
const root = document.documentElement
|
|
425
|
+
if (ov?.enabled) {
|
|
426
|
+
root.classList.add('ow-native-overlay')
|
|
427
|
+
root.style.setProperty('--ow-overlay-height', `${ov.height}px`)
|
|
428
|
+
root.style.setProperty('--ow-overlay-width', `${ov.width}px`)
|
|
429
|
+
} else {
|
|
430
|
+
// Sin botones nativos: mostramos los del web (.ow-web-controls).
|
|
431
|
+
root.classList.add('ow-web-controls')
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
103
435
|
function wireTitlebar(): void {
|
|
104
436
|
const id = window.__owWindowId
|
|
105
437
|
$('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', id))
|
|
@@ -113,6 +445,28 @@ function wireTitlebar(): void {
|
|
|
113
445
|
// ── Arranque ────────────────────────────────────────────────────────────
|
|
114
446
|
const actions: Record<string, () => void | Promise<void>> = {
|
|
115
447
|
open: openFile,
|
|
448
|
+
multi: openMultiple,
|
|
449
|
+
save: saveAs,
|
|
450
|
+
capture: capturePage,
|
|
451
|
+
nimage: nativeImageTest,
|
|
452
|
+
menu: showMenu,
|
|
453
|
+
tray: toggleTray,
|
|
454
|
+
'theme-system': () => setTheme('system'),
|
|
455
|
+
'theme-light': () => setTheme('light'),
|
|
456
|
+
'theme-dark': () => setTheme('dark'),
|
|
457
|
+
pdf: exportPdf,
|
|
458
|
+
print: printDoc,
|
|
459
|
+
child: childWindow,
|
|
460
|
+
ontop: toggleOnTop,
|
|
461
|
+
prog: progressBar,
|
|
462
|
+
ignore: ignoreMouse,
|
|
463
|
+
wins: listWindows,
|
|
464
|
+
screens: showScreens,
|
|
465
|
+
nearest: nearestDisplay,
|
|
466
|
+
'power-on': powerOn,
|
|
467
|
+
blocker: toggleBlocker,
|
|
468
|
+
idle: idleInfo,
|
|
469
|
+
ask: askDialog,
|
|
116
470
|
notify: notifyUser,
|
|
117
471
|
copy: copyPath,
|
|
118
472
|
reveal: revealPath,
|
|
@@ -123,15 +477,149 @@ const actions: Record<string, () => void | Promise<void>> = {
|
|
|
123
477
|
},
|
|
124
478
|
}
|
|
125
479
|
|
|
480
|
+
// ── Navegador embebido (webview nativa vía API `webview.*`) ─────────────
|
|
481
|
+
let wvId: number | null = null
|
|
482
|
+
|
|
483
|
+
/** Coloca la webview nativa sobre #wv-slot (y la oculta si el slot sale de vista). */
|
|
484
|
+
function syncBrowserBounds(): void {
|
|
485
|
+
if (wvId == null) return
|
|
486
|
+
const slot = document.getElementById('wv-slot')
|
|
487
|
+
if (!slot) return
|
|
488
|
+
const r = slot.getBoundingClientRect()
|
|
489
|
+
const onScreen =
|
|
490
|
+
r.bottom > 0 && r.top < window.innerHeight && r.width > 8 && r.height > 8
|
|
491
|
+
void invoke('webview', 'setBounds', wvId, {
|
|
492
|
+
x: Math.round(r.left),
|
|
493
|
+
y: Math.round(r.top),
|
|
494
|
+
width: Math.round(r.width),
|
|
495
|
+
height: Math.round(r.height),
|
|
496
|
+
})
|
|
497
|
+
void invoke('webview', 'setVisible', wvId, onScreen)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function wireBrowser(): Promise<void> {
|
|
501
|
+
const urlInput = $('#wv-url') as HTMLInputElement
|
|
502
|
+
const status = $('#wv-status')
|
|
503
|
+
try {
|
|
504
|
+
const res = await invoke<{ id: number }>('webview', 'create', {
|
|
505
|
+
url: urlInput.value,
|
|
506
|
+
x: 0,
|
|
507
|
+
y: 0,
|
|
508
|
+
width: 100,
|
|
509
|
+
height: 100,
|
|
510
|
+
})
|
|
511
|
+
wvId = res.id
|
|
512
|
+
} catch (e) {
|
|
513
|
+
log(`webview no disponible: ${e}`, 'err')
|
|
514
|
+
status.textContent = 'no disponible'
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const nav = (op: string, ...args: unknown[]): void => {
|
|
519
|
+
if (wvId != null) void invoke('webview', op, wvId, ...args)
|
|
520
|
+
}
|
|
521
|
+
const go = (): void => {
|
|
522
|
+
let u = urlInput.value.trim()
|
|
523
|
+
if (u && !/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) u = `https://${u}`
|
|
524
|
+
nav('load', u)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
$('#wv-back').addEventListener('click', () => nav('back'))
|
|
528
|
+
$('#wv-fwd').addEventListener('click', () => nav('forward'))
|
|
529
|
+
$('#wv-reload').addEventListener('click', () => nav('reload'))
|
|
530
|
+
$('#wv-go').addEventListener('click', go)
|
|
531
|
+
urlInput.addEventListener('keydown', (e) => {
|
|
532
|
+
if (e.key === 'Enter') go()
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
window.ow.on('webview.urlChanged', (p) => {
|
|
536
|
+
const d = p as { id: number; url: string }
|
|
537
|
+
if (d.id === wvId && d.url) urlInput.value = d.url
|
|
538
|
+
})
|
|
539
|
+
window.ow.on('webview.titleChanged', (p) => {
|
|
540
|
+
const d = p as { id: number; title: string }
|
|
541
|
+
if (d.id === wvId) status.textContent = d.title || '—'
|
|
542
|
+
})
|
|
543
|
+
window.ow.on('webview.loadChanged', (p) => {
|
|
544
|
+
const d = p as { id: number; state: string }
|
|
545
|
+
if (d.id === wvId) status.textContent = d.state
|
|
546
|
+
})
|
|
547
|
+
window.ow.on('webview.loadFailed', (p) => {
|
|
548
|
+
const d = p as { id: number; message: string }
|
|
549
|
+
if (d.id === wvId) log(`webview error: ${d.message}`, 'err')
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
const page = $('.page')
|
|
553
|
+
page.addEventListener('scroll', syncBrowserBounds, { passive: true } as AddEventListenerOptions)
|
|
554
|
+
window.addEventListener('resize', syncBrowserBounds)
|
|
555
|
+
requestAnimationFrame(syncBrowserBounds)
|
|
556
|
+
}
|
|
557
|
+
|
|
126
558
|
function boot(): void {
|
|
559
|
+
applyTitleBarOverlay()
|
|
127
560
|
wireTitlebar()
|
|
128
561
|
renderEnvironment()
|
|
562
|
+
void renderApp()
|
|
563
|
+
void loadTheme()
|
|
564
|
+
void wireBrowser()
|
|
565
|
+
|
|
566
|
+
// Eventos de ciclo de vida de la app (C1): pueden llegar en cualquier momento.
|
|
567
|
+
window.ow.on('app.event', (name) => log(`app.event → ${name}`, 'ok'))
|
|
568
|
+
// Eventos de webContents (C3) reenviados por el main.
|
|
569
|
+
window.ow.on('webContents.event', (name) => log(`webContents → ${name}`, 'ok'))
|
|
570
|
+
// Clicks de menú (C5).
|
|
571
|
+
window.ow.on('menu.event', (name) => log(`menu → ${name}`, 'ok'))
|
|
572
|
+
// Eventos del tray (C6).
|
|
573
|
+
window.ow.on('tray.event', (b) => log(`tray → ${b}`, 'ok'))
|
|
574
|
+
// Cambios de tema (C7).
|
|
575
|
+
window.ow.on('theme.changed', (t) => {
|
|
576
|
+
const dark = (t as { dark?: boolean })?.dark
|
|
577
|
+
applyTheme(!!dark)
|
|
578
|
+
log(`theme.changed → dark=${dark}`, 'ok')
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// C10 — pantallas: eventos nativos (requieren haber pulsado «Pantallas»).
|
|
582
|
+
window.ow.on('screen.added', (p) => {
|
|
583
|
+
log(`screen.added → #${(p as { display?: { id?: number } })?.display?.id}`, 'ok')
|
|
584
|
+
void showScreens()
|
|
585
|
+
})
|
|
586
|
+
window.ow.on('screen.removed', (p) => {
|
|
587
|
+
log(`screen.removed → #${(p as { display?: { id?: number } })?.display?.id}`, 'ok')
|
|
588
|
+
void showScreens()
|
|
589
|
+
})
|
|
590
|
+
window.ow.on('screen.changed', (p) => {
|
|
591
|
+
const d = p as { display?: { id?: number }; metrics?: string[] }
|
|
592
|
+
log(`screen.changed → #${d?.display?.id} [${(d?.metrics ?? []).join(', ')}]`, 'ok')
|
|
593
|
+
void showScreens()
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
// C10 — energía.
|
|
597
|
+
window.ow.on('power.suspend', () => appendPower('suspend'))
|
|
598
|
+
window.ow.on('power.resume', () => appendPower('resume'))
|
|
599
|
+
window.ow.on('power.ac', () => {
|
|
600
|
+
$('#power-status').textContent = 'AC'
|
|
601
|
+
appendPower('on-ac')
|
|
602
|
+
})
|
|
603
|
+
window.ow.on('power.battery', () => {
|
|
604
|
+
$('#power-status').textContent = 'batería'
|
|
605
|
+
appendPower('on-battery')
|
|
606
|
+
})
|
|
607
|
+
window.ow.on('power.shutdown', () => appendPower('shutdown'))
|
|
608
|
+
window.ow.on('power.lock', () => appendPower('lock-screen'))
|
|
609
|
+
window.ow.on('power.unlock', () => appendPower('unlock-screen'))
|
|
129
610
|
|
|
130
611
|
for (const btn of document.querySelectorAll<HTMLElement>('[data-action]')) {
|
|
131
612
|
const action = actions[btn.dataset.action ?? '']
|
|
132
613
|
if (!action) continue
|
|
614
|
+
let busy = false
|
|
133
615
|
btn.addEventListener('click', () => {
|
|
134
|
-
|
|
616
|
+
if (busy) return
|
|
617
|
+
busy = true
|
|
618
|
+
Promise.resolve(action())
|
|
619
|
+
.catch(logError)
|
|
620
|
+
.finally(() => {
|
|
621
|
+
busy = false
|
|
622
|
+
})
|
|
135
623
|
})
|
|
136
624
|
}
|
|
137
625
|
|