@pmate/cli 0.34.1 → 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.
package/dist/index.js CHANGED
@@ -17612,7 +17612,7 @@ async function buildDesktop(args) {
17612
17612
  files: ["**/*"],
17613
17613
  asar: true,
17614
17614
  artifactName: "${productName}-${version}-${arch}.${ext}",
17615
- mac: { icon, target: args.dir ? ["dir"] : ["dmg", "zip"], category: "public.app-category.social-networking", hardenedRuntime: args.signed, extendInfo: { NSMicrophoneUsageDescription: `${c.productName} uses your microphone only when you record a voice message.` }, ...args.signed ? {} : { identity: "-", notarize: false } },
17615
+ mac: { icon, target: args.dir ? ["dir"] : ["dmg", "zip"], category: "public.app-category.social-networking", hardenedRuntime: args.signed, extendInfo: { NSMicrophoneUsageDescription: `${c.productName} uses your microphone for voice calls and voice messages.` }, ...args.signed ? {} : { identity: "-", notarize: false } },
17616
17616
  forceCodeSigning: args.signed
17617
17617
  };
17618
17618
  const configPath = import_node_path23.default.join(staging, "builder.json");
@@ -1,6 +1,7 @@
1
- const { app, BrowserWindow, session, shell, Menu } = 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
+ const { installMediaPermissions } = require('./media.cjs')
4
5
  const config = require('./desktop-config.json')
5
6
  const manifest = require('./resource-manifest.json')
6
7
  app.setName(config.productName)
@@ -23,7 +24,7 @@ function restrict(window) {
23
24
  window.webContents.on('did-create-window', child => restrict(child))
24
25
  }
25
26
  function createWindow() {
26
- 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 } })
27
28
  restrict(mainWindow)
28
29
  mainWindow.webContents.on('did-fail-load', (_event, code, _description, url, mainFrame) => {
29
30
  if (mainFrame && code !== -3 && url !== `${config.origin}/_desktop/offline`) void mainWindow.loadURL(`${config.origin}/_desktop/offline`)
@@ -44,12 +45,12 @@ else {
44
45
  }
45
46
  return resources(request) ?? desktopSession.fetch(request, { bypassCustomProtocolHandlers: true })
46
47
  })
47
- desktopSession.setPermissionRequestHandler((contents, permission, callback, details) => {
48
- let trusted = false
49
- try { trusted = !!contents && new URL(contents.getURL()).origin === config.origin } catch {}
50
- callback(Boolean(trusted && permission === 'media' && details.mediaTypes?.length && details.mediaTypes.every(type => type === 'audio')))
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
51
53
  })
52
- desktopSession.setPermissionCheckHandler((_contents, permission, origin, details) => origin === config.origin && permission === 'media' && details.mediaType === 'audio')
53
54
  Menu.setApplicationMenu(Menu.buildFromTemplate([
54
55
  { role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
55
56
  { label: config.productName, submenu: [{ label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin) } }] },
@@ -0,0 +1,33 @@
1
+ const { chooseScreen } = require('./share-picker.cjs')
2
+ function installMediaPermissions(desktopSession, { origin, getWindow, desktopCapturer, BrowserWindow, ipcMain }) {
3
+ const sameOrigin = raw => { try { return new URL(raw).origin === origin } catch { return false } }
4
+ const trusted = contents => {
5
+ const window = getWindow()
6
+ return !!window && !window.isDestroyed() && contents === window.webContents && sameOrigin(contents.getURL())
7
+ }
8
+ desktopSession.setPermissionRequestHandler((contents, permission, callback, details) => {
9
+ const allowed = trusted(contents) && details.isMainFrame === true && sameOrigin(details.requestingUrl)
10
+ callback(Boolean(allowed && (permission === 'display-capture' || (permission === 'media' && details.mediaTypes?.length && details.mediaTypes.every(type => type === 'audio')))))
11
+ })
12
+ desktopSession.setPermissionCheckHandler((contents, permission, requestingOrigin, details) => {
13
+ return Boolean(trusted(contents) && sameOrigin(requestingOrigin) && details.isMainFrame === true && (permission === 'display-capture' || (permission === 'media' && details.mediaType === 'audio')))
14
+ })
15
+ desktopSession.setDisplayMediaRequestHandler(async (request, callback) => {
16
+ const window = getWindow()
17
+ const valid = () => window && !window.isDestroyed() && trusted(window.webContents) && request.frame && request.frame === window.webContents.mainFrame && sameOrigin(request.securityOrigin)
18
+ if (!valid() || !request.userGesture || !request.videoRequested || request.audioRequested) return callback(null)
19
+ let completed = false
20
+ const finish = source => {
21
+ if (completed) return
22
+ completed = true
23
+ try { callback(source && valid() ? { video: source } : null) } catch { /* Request frame was closed or navigated. */ }
24
+ }
25
+ try {
26
+ const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 200 } })
27
+ if (!valid() || !sources.length) return finish(null)
28
+ const source = await chooseScreen({ BrowserWindow, ipcMain, parent: window, sources })
29
+ finish(source)
30
+ } catch { finish(null) }
31
+ }, { useSystemPicker: false })
32
+ }
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.34.1",
3
+ "version": "0.35.1",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {