@pmate/cli 0.35.0 → 0.35.1

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.
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, session, shell, Menu, desktopCapturer } = require('electron')
1
+ const { app, BrowserWindow, session, shell, Menu, desktopCapturer, ipcMain } = require('electron')
2
2
  const path = require('node:path')
3
3
  const { createResourceHandler } = require('./resources.cjs')
4
4
  const { installMediaPermissions } = require('./media.cjs')
@@ -24,7 +24,7 @@ function restrict(window) {
24
24
  window.webContents.on('did-create-window', child => restrict(child))
25
25
  }
26
26
  function createWindow() {
27
- mainWindow = new BrowserWindow({ width: 1180, height: 820, minWidth: 380, minHeight: 540, title: config.productName, backgroundColor: '#f6f8fb', webPreferences: { partition: 'persist:desktop', nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true } })
27
+ mainWindow = new BrowserWindow({ width: 1180, height: 820, minWidth: 380, minHeight: 540, title: config.productName, backgroundColor: '#f6f8fb', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), partition: 'persist:desktop', nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true } })
28
28
  restrict(mainWindow)
29
29
  mainWindow.webContents.on('did-fail-load', (_event, code, _description, url, mainFrame) => {
30
30
  if (mainFrame && code !== -3 && url !== `${config.origin}/_desktop/offline`) void mainWindow.loadURL(`${config.origin}/_desktop/offline`)
@@ -45,7 +45,12 @@ else {
45
45
  }
46
46
  return resources(request) ?? desktopSession.fetch(request, { bypassCustomProtocolHandlers: true })
47
47
  })
48
- installMediaPermissions(desktopSession, { origin: config.origin, getWindow: () => mainWindow, desktopCapturer, Menu })
48
+ installMediaPermissions(desktopSession, { origin: config.origin, getWindow: () => mainWindow, desktopCapturer, BrowserWindow, ipcMain })
49
+ ipcMain.handle('pmate:screen-settings', event => {
50
+ if (!mainWindow || event.sender !== mainWindow.webContents || event.senderFrame !== mainWindow.webContents.mainFrame || new URL(mainWindow.webContents.getURL()).origin !== config.origin) return false
51
+ if (process.platform === 'darwin') { void shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); return true }
52
+ return false
53
+ })
49
54
  Menu.setApplicationMenu(Menu.buildFromTemplate([
50
55
  { role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
51
56
  { label: config.productName, submenu: [{ label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin) } }] },
@@ -1,4 +1,5 @@
1
- function installMediaPermissions(desktopSession, { origin, getWindow, desktopCapturer, Menu }) {
1
+ const { chooseScreen } = require('./share-picker.cjs')
2
+ function installMediaPermissions(desktopSession, { origin, getWindow, desktopCapturer, BrowserWindow, ipcMain }) {
2
3
  const sameOrigin = raw => { try { return new URL(raw).origin === origin } catch { return false } }
3
4
  const trusted = contents => {
4
5
  const window = getWindow()
@@ -22,17 +23,11 @@ function installMediaPermissions(desktopSession, { origin, getWindow, desktopCap
22
23
  try { callback(source && valid() ? { video: source } : null) } catch { /* Request frame was closed or navigated. */ }
23
24
  }
24
25
  try {
25
- const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 0, height: 0 } })
26
+ const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 200 } })
26
27
  if (!valid() || !sources.length) return finish(null)
27
- // Older macOS versions have no system picker. Native menu selection remains explicit.
28
- const menu = Menu.buildFromTemplate([
29
- { label: 'Choose a screen or window to share', enabled: false },
30
- { type: 'separator' },
31
- ...sources.map(source => ({ label: source.name.replace(/&/g, '&&'), click: () => finish(source) })),
32
- { type: 'separator' }, { label: 'Cancel', click: () => finish(null) },
33
- ])
34
- menu.popup({ window, callback: () => finish(null) })
28
+ const source = await chooseScreen({ BrowserWindow, ipcMain, parent: window, sources })
29
+ finish(source)
35
30
  } catch { finish(null) }
36
- }, { useSystemPicker: true })
31
+ }, { useSystemPicker: false })
37
32
  }
38
33
  module.exports = { installMediaPermissions }
@@ -0,0 +1,2 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+ contextBridge.exposeInMainWorld('pmateDesktop', { openScreenSettings: () => ipcRenderer.invoke('pmate:screen-settings') })
@@ -0,0 +1,3 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+ const nonce = process.argv.find(arg => arg.startsWith('--picker-id='))?.slice('--picker-id='.length)
3
+ contextBridge.exposeInMainWorld('screenPicker', { sources: () => ipcRenderer.invoke(`screen-picker:data:${nonce}`), select: id => ipcRenderer.invoke(`screen-picker:select:${nonce}`, typeof id === 'string' ? id : null) })
@@ -0,0 +1,22 @@
1
+ const { randomUUID } = require('node:crypto')
2
+ const path = require('node:path')
3
+ /** A separate sandboxed window owns the picker; only that exact frame can select. */
4
+ function chooseScreen({ BrowserWindow, ipcMain, parent, sources }) {
5
+ return new Promise(resolve => {
6
+ const nonce = randomUUID()
7
+ const picker = new BrowserWindow({ parent, modal: true, width: 780, height: 550, minWidth: 540, minHeight: 420, title: '选择共享内容', backgroundColor: '#f6faf8', autoHideMenuBar: true,
8
+ webPreferences: { preload: path.join(__dirname, 'share-picker-preload.cjs'), additionalArguments: [`--picker-id=${nonce}`], partition: `screen-picker-${nonce}`, nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true } })
9
+ let complete = false
10
+ const dataChannel = `screen-picker:data:${nonce}`, selectChannel = `screen-picker:select:${nonce}`
11
+ const trusted = event => event.sender === picker.webContents && event.senderFrame === picker.webContents.mainFrame
12
+ const finish = source => { if (complete) return; complete = true; ipcMain.removeHandler(dataChannel); ipcMain.removeHandler(selectChannel); resolve(source); if (!picker.isDestroyed()) picker.close() }
13
+ ipcMain.handle(dataChannel, event => trusted(event) ? sources.map(s => ({ id: s.id, name: s.name, thumbnail: s.thumbnail?.toDataURL() ?? '', kind: s.id.startsWith('screen:') ? 'screen' : 'window' })) : [])
14
+ ipcMain.handle(selectChannel, (event, id) => { if (!trusted(event)) return; finish(sources.find(s => s.id === id)) })
15
+ picker.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
16
+ picker.webContents.on('will-navigate', event => event.preventDefault())
17
+ picker.once('closed', () => finish())
18
+ picker.webContents.on('render-process-gone', () => finish())
19
+ picker.loadFile(path.join(__dirname, 'share-picker.html')).catch(() => finish())
20
+ })
21
+ }
22
+ module.exports = { chooseScreen }
@@ -0,0 +1,3 @@
1
+ <!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; img-src data:"><title>选择共享内容</title><style>
2
+ *{box-sizing:border-box}body{margin:0;font:14px -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f6faf8;color:#172c27}main{height:100vh;display:flex;flex-direction:column;padding:24px}h1{font-size:19px;margin:0 0 20px}nav{display:flex;gap:8px;margin-bottom:20px}button{font:inherit;cursor:pointer;border:1px solid #dce6e1;background:white;color:#19372d;border-radius:9px;padding:10px 20px}button:focus-visible{outline:3px solid #2ec88a;outline-offset:3px}button[aria-selected=true]{background:#e1f6ed;color:#087d52;border-color:#9ddbbf}#sources{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;overflow:auto;flex:1;align-content:start;padding:4px}.source{padding:5px;min-width:0;border:2px solid transparent;background:transparent}.source[aria-checked=true]{border-color:#149964;background:#e6f7ef}.source img{width:100%;height:105px;object-fit:contain;background:#e5ece8;border-radius:5px}.source span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:8px 2px;font-size:12px}footer{padding-top:20px;display:flex;align-items:center;gap:10px}footer p{flex:1;color:#6a7c74;font-size:12px}#start{background:#11875c;color:#fff;border-color:#11875c}button:disabled{opacity:.45;cursor:default}#error{color:#687d73;line-height:1.7} @media(prefers-color-scheme:dark){body{background:#14231e;color:#edf6f1}button{background:#20372e;color:#edf6f1;border-color:#456353}.source img{background:#23362e}.source[aria-checked=true]{background:#234c3c}}
3
+ </style></head><body><main><h1>选择共享内容</h1><nav role="tablist"><button role="tab" data-kind="screen" aria-selected="true">整个屏幕</button><button role="tab" data-kind="window" aria-selected="false">应用窗口</button></nav><div id="sources" role="radiogroup" aria-label="共享来源"></div><p id="error" hidden></p><footer><p>只有点击开始共享后,对方才能看到内容</p><button id="cancel">取消</button><button id="start" disabled>开始共享</button></footer></main><script src="share-picker.js"></script></body></html>
@@ -0,0 +1,20 @@
1
+ let sources = [], kind = 'screen', selected
2
+ const list = document.getElementById('sources'), start = document.getElementById('start')
3
+ function render() {
4
+ list.replaceChildren()
5
+ const available = sources.filter(source => source.kind === kind)
6
+ for (const source of available) {
7
+ const button = document.createElement('button'); button.className = 'source'; button.setAttribute('role', 'radio'); button.setAttribute('aria-checked', String(selected === source.id))
8
+ const image = document.createElement('img'); image.src = source.thumbnail; image.alt = ''
9
+ const label = document.createElement('span'); label.textContent = source.name; button.title = source.name
10
+ button.append(image, label); button.onclick = () => { selected = source.id; render() }; list.append(button)
11
+ }
12
+ start.disabled = !available.some(source => source.id === selected)
13
+ document.getElementById('error').hidden = available.length > 0
14
+ document.getElementById('error').textContent = '没有可共享的内容。请在系统设置中允许屏幕录制,再重新打开应用。'
15
+ }
16
+ for (const tab of document.querySelectorAll('[data-kind]')) tab.onclick = () => { kind = tab.dataset.kind; selected = undefined; for (const other of document.querySelectorAll('[data-kind]')) other.setAttribute('aria-selected', String(other === tab)); render() }
17
+ document.getElementById('cancel').onclick = () => window.screenPicker.select(null)
18
+ start.onclick = () => { if (selected) { start.disabled = true; window.screenPicker.select(selected) } }
19
+ document.addEventListener('keydown', e => { if (e.key === 'Escape') window.screenPicker.select(null) })
20
+ window.screenPicker.sources().then(data => { sources = data; render() }).catch(() => render())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {