/** * {注入的js} * @author yutent * @date 2023/07/21 17:38:11 */ const MIME_TYPES = { html: 'text/html', json: 'application/json', js: 'application/javascript', htm: 'text/html', txt: 'text/plain', css: 'text/css', webp: 'image/webp', jpg: 'image/jpg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', svg: 'image/svg+xml', ico: 'image/ico', mp3: 'audio/mpeg', ogg: 'audio/ogg', m4a: 'audio/m4a', amr: 'audio/amr', mp4: 'video/mp4', webm: 'video/webm', wasm: 'application/wasm', asm: 'application/asm', zip: 'application/zip', '7z': 'application/x-7z-compressed', eot: 'application/vnd.ms-fontobject', ttf: 'font/ttf', otf: 'font/otf', woff: 'font/woff', woff2: 'font/woff2', xls: 'application/vnd.ms-excel', doc: 'application/msword', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } const KEYS_MAP = { shift: '', ctrl: '', alt: '', super: '' } function defer() { let obj = {} obj.promise = new Promise((resolve, reject) => { obj.resolve = resolve obj.reject = reject }) return obj } function rand(prefix = 'cb_') { return prefix + Math.random().toString().slice(2) } function handler(event, data = {}, once = true) { let _ = defer() let callback if (typeof once === 'boolean') { callback = rand() native[once ? '$once' : '$on'](callback, _.resolve) } else { _.resolve(true) } window.webkit.messageHandlers.app.postMessage({ event, data, callback }) return _.promise } function base64(str = '') { return btoa(str).replace(/[+=\/]/g, '') } class NativeImage { #origin constructor(obj) { this.#origin = obj this.width = obj.width this.height = obj.height this.type = MIME_TYPES[obj.filepath.split('.').pop()] } toJSON() { return this.#origin } export(type, base64) { let _ = defer() let canvas = document.createElement('canvas') canvas.width = this.width canvas.height = this.height let ctx = canvas.getContext('2d') let imgData = ctx.getImageData(0, 0, this.width, this.height) let data = imgData.data for (let i = 0; i < this.#origin.bytes.length; i += 4) { imgData.data[i] = this.#origin.bytes[i] imgData.data[i + 1] = this.#origin.bytes[i + 1] imgData.data[i + 2] = this.#origin.bytes[i + 2] imgData.data[i + 3] = this.#origin.bytes[i + 3] } ctx.putImageData(imgData, 0, 0) if (base64) { return canvas.toDataURL(type || this.type, 1) } else { canvas.toBlob(_.resolve, type || this.type, 1) return _.promise } } toPNG() { return this.export('image/png') } toJPEG() { return this.export('image/jpeg') } toDataURL(type) { return this.export(type, true) } } class EventEmitter { // __events__ = Object.create(null) $on(name, fn) { if (this.__events__[name]) { this.__events__[name].push(fn) } else { this.__events__[name] = [fn] } } $once(name, fn) { fn.__once__ = true this.$on(name, fn) } $off(name, fn) { if (this.__events__[name]) { if (fn) { this.__events__[name] = this.__events__[name].filter(it => it !== fn) } else { this.__events__[name] = [] } } } $emit(name, ...args) { if (this.__events__[name]) { for (let fn of this.__events__[name]) { try { fn.apply(this, args) if (fn.__once__) { this.$off(name, fn) } } catch (e) { console.error(e) } } } } $destroy() { this.__events__ = Object.create(null) } } window.native = new EventEmitter() Object.assign(native, { quit() { return handler('quit', {}, null) }, fs: { read(filepath, mode = 'r') { return handler('fs', { action: 'read', mode, filepath }).then(r => mode.includes('b') ? new Uint8Array(r) : r ) }, write(filepath, content = '', mode = 'w') { return handler('fs', { action: 'write', append: false, filepath, content }) }, append(filepath, content = '', mode = 'w') { return handler('fs', { action: 'write', append: true, filepath, content }) }, exists(filepath) { return handler('fs', { action: 'exists', filepath }) }, list(filepath) { return handler('fs', { action: 'list', filepath }) }, isfile(filepath) { return handler('fs', { action: 'isfile', filepath }) }, isdir(filepath) { return handler('fs', { action: 'isdir', filepath }) }, remove(filepath) { return handler('fs', { action: 'remove', filepath }) }, rename(filepath, target) { return handler('fs', { action: 'rename', filepath, target }) }, copy(filepath, target) { return handler('fs', { action: 'copy', filepath, target }) }, async upload(file) { return handler('fs', { action: 'upload', file: new Uint8Array(await file.arrayBuffer()) }) } }, image(filepath) { return handler('image', { value: filepath }).then(r => new NativeImage(r)) }, clipboard: { readText() { return handler('clipboard', { action: 'wait_for_text' }) }, writeText(value) { return handler('clipboard', { action: 'set_text', value }, null) }, readImage() { return handler('clipboard', { action: 'wait_for_image' }).then(r => r ? new NativeImage(r) : r ) }, writeImage(value) { // 暂不知原因, postMessage传给Gtk后, JSON.stringify()并未读取toJSON的结果 if (typeof value === 'object') { value = value.toJSON() } return handler('clipboard', { action: 'set_image', value }, null) }, clear() { return handler('clipboard', { action: 'clear' }) } }, screen: { getAllDisplays() { return handler('monitor', { action: 'get-all' }) }, getPrimaryDisplay() { return handler('monitor', { action: 'get-primary' }) } }, globalShortcut: { get enabled() { return handler('keybinder', { action: 'supported' }) }, register(keyMap, callback) { let shortcut_callback = base64(keyMap) native.$off(shortcut_callback) native.$on(shortcut_callback, callback) return handler('keybinder', { action: 'register', value: keyMap, shortcut_callback }) }, unregister(keyMap) { let shortcut_callback = base64(keyMap) native.$off(shortcut_callback) return handler('keybinder', { action: 'unregister', value: keyMap }) }, unregisterAll(keyMaps) { for (let it of keyMaps) { this.unregister(it) } } }, tray: { create() { // }, remove() { // }, /** * 设置普通状态的tray图标, 只需要传名称, 自动会去当前主题下去找 */ set_icon(name) { return handler('tray', { action: 'set_icon', value: name }) }, /** * 设置警示图标, 同上 */ set_attention_icon(name) { return handler('tray', { action: 'set_attention_icon', value: name }) }, /** * */ set_title(title) { return handler('tray', { action: 'set_title', value: title }) }, /** * 修改tray图标状态 * @param status 0: 隐藏, 1: 显示, 2: 重要(对应上面的attention_icon) */ set_status(status) { return handler('tray', { action: 'set_status', value: status }) } }, window: { isVisible() { return handler('window', { action: 'is_visible' }) }, toggleVisible() { handler('window', { action: 'toggle_visible' }, null) }, hide() { handler('window', { action: 'hide' }, null) }, show() { handler('window', { action: 'show' }, null) }, fullscreen() { handler('window', { action: 'fullscreen' }, null) }, unfullscreen() { handler('window', { action: 'unfullscreen' }, null) }, maximize() { handler('window', { action: 'maximize' }, null) }, unmaximize() { handler('window', { action: 'unmaximize' }, null) }, setTitle(title = '') { handler('window', { action: 'set_title', value: title }, null) }, resize(width = 0, height = 0) { handler('window', { action: 'resize', value: { width, height } }, null) }, move(x = 0, y = 0) { handler('window', { action: 'resize', value: { x, y } }, null) }, setOpacity(opacity = 1) { handler('window', { action: 'set_opacity', value: opacity }, null) }, alwayOnTop(setting = true) { handler('window', { action: 'set_keep_above', value: setting }, null) }, alwayOnBotttom(setting = true) { handler('window', { action: 'set_keep_below', value: setting }, null) } }, notify({ title, summary, icon, progress = 0, urgency = 0, callback }) { let eventName if (callback) { eventName = rand() native.$once(eventName, callback) } handler( 'notify', { title, summary, icon, progress, urgency, callback: eventName }, null ) }, handler })