commit 7efeeed484642bea21144ca5565f48aa798c5426 Author: yutent Date: Thu Aug 31 16:57:55 2023 +0800 0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b187fbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.txt \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..efe3129 --- /dev/null +++ b/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +if [ -d unpack ]; then + sudo rm -rf unpack +fi + +version="0.1.0" + +mkdir -p unpack/DEBIAN + +cp debian/control unpack/DEBIAN/ +cp -r usr unpack/ + +cd unpack +find . -type f | xargs md5sum > DEBIAN/md5sums + +cd .. +dpkg-deb -b unpack/ "python3-webengine-gtk3-${version}.deb" + +sudo rm -rf unpack \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..aa65fed --- /dev/null +++ b/debian/control @@ -0,0 +1,12 @@ +Package: python3-webengine-gtk3 +Version: 0.1.0 +Section: develop +Priority: optional +Maintainer: Yutent +Architecture: all +Depends: python3 (>=3.10), python3-gi, gir1.2-gtk-3.0, python3-pil, python3-gi-cairo, gir1.2-gdkpixbuf-2.0 +Recommends: gir1.2-ayatanaappindicator3-0.1, gir1.2-notify-0.7, gir1.2-keybinder-3.0 +Installed-Size: 5644 +Homepage: https://git.wkit.fun/appcat/python3-webengine-gtk3 +Description: 一个傻瓜式的webview定制库. + 基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力. diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/__init__.py b/usr/lib/python3/dist-packages/webengine/gtk3/__init__.py new file mode 100644 index 0000000..3df6f4f --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# @author yutent +# @date 2023/08/31 11:55:25 + +from webengine.gtk3._webengine import WebEngine +from webengine.gtk3._settings import create_setting + + +build = (0, 1, 0) +version = '.'.join(map(str, build)) diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_inject.py b/usr/lib/python3/dist-packages/webengine/gtk3/_inject.py new file mode 100644 index 0000000..1c73b32 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_inject.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# @author yutent +# @date 2023/08/08 15:00:27 + + +import os, gi + +gi.require_version("WebKit2", "4.1") + +from gi.repository import WebKit2 + + +class Inject: + def __init__(self, webview): + + self.manager = webview.get_user_content_manager() + + script = open(self.abspath('./inject.js'), 'r').read() + frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES + time = WebKit2.UserScriptInjectionTime.END + script = WebKit2.UserScript(script, frame, time, None, None) + + self.manager.add_script(script) + + + def connect(self, callback): + self.manager.connect('script-message-received::app', callback) + self.manager.register_script_message_handler('app') + + + def abspath(self, filepath): + root = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(root, filepath) \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_mimetypes.py b/usr/lib/python3/dist-packages/webengine/gtk3/_mimetypes.py new file mode 100644 index 0000000..813186f --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_mimetypes.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +mime_types = { + 'html': 'text/html', + 'txt': 'text/plain', + 'css': 'text/css', + 'xml': 'text/xml', + 'gif': 'image/gif', + 'jpg': 'image/jpeg', + 'webp': 'image/webp', + 'tiff': 'image/tiff', + 'png': 'image/png', + 'svg': 'image/svg+xml', + 'ico': 'image/x-icon', + 'bmp': 'image/x-ms-bmp', + 'js': 'application/javascript', + 'json': 'application/json', + 'mp3': 'audio/mpeg', + 'ogg': 'audio/ogg', + 'm4a': 'audio/x-m4a', + 'mp4': 'video/mp4', + 'webm': 'video/webm', + 'ttf': 'font/font-ttf', + 'woff': 'font/font-woff', + 'woff2': 'font/font-woff2', + 'wast': 'application/wast', + 'wasm': 'application/wasm', + 'other': 'application/octet-stream' +} + +def get_mimetype(name): + return mime_types.get(name) or mime_types['html'] \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_notify.py b/usr/lib/python3/dist-packages/webengine/gtk3/_notify.py new file mode 100644 index 0000000..88d70db --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_notify.py @@ -0,0 +1,56 @@ + +import gi + +try: + gi.require_version('Notify', '0.7') # gir1.2-notify-0.7 + from gi.repository import Notify +except: + Notify = None + + +class Notification(): + + notify = None + + def __init__(self, webview): + self.webview = webview + + if Notify: + Notify.init(Notify.get_app_name() or 'webapp') + self.notify = Notify.Notification() + + + def create(self, title, summary, icon, progress = 0, urgency = 0, callback = None): + + if not self.notify: + raise ImportError('Notify module not found. Need to install gir1.2-notify-0.7 if you use debian.') + return + + self.notify.clear_actions() + self.notify.clear_hints() + + self.notify.update(title, summary, icon) + + if progress: + self.notify.set_hint('value', progress) + + self.notify.set_urgency(urgency) + + if callback: + self.notify.add_action("click", "click", self.action_callback, callback) + + self.notify.show() + + + def action_callback(self, instance, action, callback): + if callback: + self.webview.call_js(callback) + instance.close() + + +def create_notify(): + + def wrapper(app, extra = None): + Notification(app) + + return wrapper \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_protocal.py b/usr/lib/python3/dist-packages/webengine/gtk3/_protocal.py new file mode 100644 index 0000000..4cae122 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_protocal.py @@ -0,0 +1,56 @@ + +import os +from gi.repository import Gio, WebKit2 +from ._mimetypes import get_mimetype + + +class Protocal: + root = '' + + def __init__(self, webroot = ''): + self.root = os.path.realpath(webroot) + + def register(self, webview, name = 'app'): + self.protocal = name + ctx = webview.get_context() + ctx.register_uri_scheme(name, self.handle_response) + # 允许网页通知权限 + ctx.initialize_notification_permissions([WebKit2.SecurityOrigin.new_for_uri('app:///index.html')], []) + + + def abspath(self, filepath): + return os.path.join(self.root, filepath) + + + def handle_response(self, req): + schema = req.get_scheme() + pathname = req.get_path()[1:] + ext = pathname.split('.')[-1] + + # print('----------------------------------------') + # print(req.get_uri(),schema, pathname, ext, get_mimetype(ext)) + # print('----------------------------------------') + + if schema == self.protocal: + data = open(self.abspath(pathname)).read() + data = Gio.MemoryInputStream.new_from_data(data.encode()) + + # ------- 更多功能的reponse ---------------- + # res = WebKit2.URISchemeResponse.new(data, -1) + # res.set_content_type(get_mimetype(ext)) + # res.set_http_headers('text/html') + # res.set_status(200) + # req.finish_with_response(res) + # ---------------------------------------- + + # 简单的response + req.finish(data, -1, get_mimetype(ext)) + + +def create_protocal(root): + proto = Protocal(root) + + def wrapper(app, extra = None): + proto.register(app) + + return wrapper \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_settings.py b/usr/lib/python3/dist-packages/webengine/gtk3/_settings.py new file mode 100644 index 0000000..d668c50 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_settings.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# @author yutent +# @date 2023/08/08 14:07:26 + + +import gi, os + +gi.require_version("WebKit2", "4.1") + +from gi.repository import WebKit2 + + +class Settings(WebKit2.Settings): + def __init__(self): + + WebKit2.Settings.__init__(self) + + self.set_enable_page_cache(True) + self.set_enable_offline_web_application_cache(True) + + self.set_javascript_can_access_clipboard(True) + self.set_javascript_can_open_windows_automatically(True) + + + self.set_user_agent_with_application_details('WebEngine', '0.1.0') + + + # indexedDB 和 localStorage 和 离线缓存 + self.set_enable_html5_database(True) + self.set_enable_html5_local_storage(True) + self.set_enable_offline_web_application_cache(True) + + # 平滑滚动 + self.set_enable_smooth_scrolling(True) + + + self.set_enable_fullscreen(True) + + # 媒体播放是否需要 用户主动行为 + self.set_media_playback_requires_user_gesture(False) + + # 允许视频播放窗口不强制全屏 + self.set_media_playback_allows_inline(True) + + + def set_useragent(self, str): + self.set_user_agent(str) + + # 关闭之后, 可以随便跨域请求, 但是有安全隐患(不过, 只要保证你页面只运行你自己的代码, 及信任的代码, 就不会有任何问题) + def disable_security(self): + self.set_disable_web_security(True) + + # 禁止全屏 (默认允许) + def disable_fullscreen(self): + self.set_enable_fullscreen(False) + + + # 启用开发者工具 + def enable_devtools(self): + self.set_enable_developer_extras(True) + + # 模拟设备 + def mock_devices(self): + # 是用于启用或禁用模拟捕获设备的设置选项。 + # 在开发网页应用程序时,经常需要使用摄像头或麦克风等捕获设备进行测试和调试。但是,在某些情况下,可能无法直接访问实际的物理捕获设备,或者不希望在开发过程中实际使用这些设备。 + # 通过启用WebKit2.Settings:enable-mock-capture-devices设置选项,可以使用虚拟的模拟捕获设备来替代实际的物理设备。这样,开发人员可以在没有真实设备的情况下进行捕获设备相关的功能测试和调试,提高开发效率并简化开发流程。 + self.set_enable_mock_capture_devices(True) + + + +def create_setting(options = None): + setting = Settings() + if options is not None: + if options.get('devtools'): + setting.enable_devtools() + + if options.get('useragent'): + setting.set_useragent(options.get('useragent')) + + if options.get('web_security') is False: + setting.disable_security() + + if options.get('disable_fullscreen'): + setting.disable_fullscreen() + + def wrapper(app, extra = None): + app.set_settings(setting) + + return wrapper \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_utils.py b/usr/lib/python3/dist-packages/webengine/gtk3/_utils.py new file mode 100644 index 0000000..8c7f2a3 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_utils.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# @author yutent +# @date 2023/07/28 14:39:33 + +import gi +from gi.repository.GdkPixbuf import Pixbuf + +def get_monitor_info(monitor): + return { + "model": monitor.props.model, + "scale_factor": monitor.props.scale_factor, + "manufacturer": monitor.props.manufacturer, + "refresh_rate": monitor.props.refresh_rate, + "is_primary": monitor.is_primary(), + "geometry": { + "width": monitor.props.geometry.width, + "height": monitor.props.geometry.height, + "x": monitor.props.geometry.x, + "y": monitor.props.geometry.y, + } + } + + +def pixbuf_to_dict(pixbuf, filename = ''): + + if pixbuf: + has_alpha = pixbuf.get_has_alpha() + + # 没有apha通道时, get_pixels() 得到像素矩阵会出现极个别像素出现alpha通道 + # 所以, 这里强制设置alpha通道为false, 补全像素矩阵的数据 + if not has_alpha: + pixbuf = pixbuf.add_alpha(False, 0, 0, 0) + + data = { + "width": pixbuf.get_width(), + "height": pixbuf.get_height(), + "colorspace": pixbuf.get_colorspace(), + "has_alpha": has_alpha, + "bits_per_sample": pixbuf.get_bits_per_sample(), + "rowstride": pixbuf.get_rowstride(), + "filepath": filename, + "bytes": list(pixbuf.get_pixels()) + } + else: + data = None + + return data + + +def dict_to_pixbuf(data): + if data: + image = Pixbuf.new_from_data( + data = bytes(data['bytes']), + colorspace = data['colorspace'], + has_alpha = data['has_alpha'], + bits_per_sample = data['bits_per_sample'], + width = data['width'], + height = data['height'], + rowstride = data['rowstride'] + ) + else: + image = None + + return image \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/_webengine.py b/usr/lib/python3/dist-packages/webengine/gtk3/_webengine.py new file mode 100644 index 0000000..8b4d8d9 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/_webengine.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# @author yutent +# @date 2023/08/08 14:07:26 + + +import gi, os, json, shutil, hashlib + +gi.require_version("WebKit2", "4.1") + + +from gi.repository import GObject, Gtk, Gdk, WebKit2, GLib, Gio, GdkPixbuf + + +# 优先尝试使用指示器, 没有再使用 Gtk.StatusIcon +try: + # gir1.2-ayatanaappindicator3-0.1 + gi.require_version('AyatanaAppIndicator3', '0.1') + from gi.repository import AyatanaAppIndicator3 as AppIndicator3 +except: + AppIndicator3 = None + +try: + # gir1.2-keybinder-3.0 + gi.require_version("Keybinder", "3.0") + from gi.repository import Keybinder + + # 初始化 Keybinder + Keybinder.init() +except: + Keybinder = None + + + + + +from ._settings import create_setting +from ._protocal import create_protocal +from ._notify import create_notify +from ._inject import Inject +from ._utils import get_monitor_info, pixbuf_to_dict, dict_to_pixbuf + + +def noop(): + pass + + +class WebEngine(WebKit2.WebView): + + __gsignals__ = { + 'quit': (GObject.SignalFlags.RUN_FIRST, None, ()) + } + + root = None + window = None + + def __init__(self, window): + + WebKit2.WebView.__init__(self) + + self.window = window + + self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + self.display = Gdk.Display.get_default() + + self.use(create_notify()) + + setting = create_setting() + self.use(setting) + + + im = self.get_input_method_context() + im.set_enable_preedit(True) + + # 解决输入法候选框跟随问题 + im.connect('preedit-started', self.on_preedit_changed) + im.connect('preedit-changed', self.on_preedit_changed) + im.connect('preedit-finished', self.on_preedit_changed) + + Inject(self).connect(self.called_by_js) + + + def set_root(self, root): + self.root = root + return self.use(create_protocal(root)) + + def use(self, middle_ware = noop, extra = None): + middle_ware(self, extra) + return self + + + def load(self, url = '/index.html'): + if self.root is None: + raise EnvironmentError('web root dir not set!') + else: + self.load_uri(f"app://{url}") + + + def on_preedit_changed(self, im): + seat = self.display.get_default_seat() + p = seat.get_pointer().get_position() # 光标位置 + x, y = self.get_position() # 窗口位置 + + im.notify_focus_in() + im.notify_cursor_area(p.x - x, p.y - y, 0, 0) # 修正输入法跟随 + + + def call_js(self, method, data = None): + scripts = 'native.$emit("' + method + '",' + json.dumps(data) + ')' + self.evaluate_javascript(scripts, -1) + + + def called_by_js(self, webview, message): + + data = json.loads(message.get_js_value().to_json(0)) + + event = data.get('event') + callback = data.get('callback') + params = data.get('data') + output = None + + + match event: + case 'fs': + filepath = params.get('filepath') + + if params['action'] == 'read': + with open(filepath, params.get('mode')) as file: + output = file.read() + if params.get('mode').find('b') > -1: + output = list(output) + + if params['action'] == 'write': + + # 调整以支持二进制数据写入 + with open(filepath, params.get('mode')) as file: + buff = params['content'] + + if params.get('mode').find('b') > -1: + buff = bytes(buff) + + output = file.write(buff) + + if params['action'] == 'exists': + output = os.path.exists(filepath) + + if params['action'] == 'list': + with os.scandir(filepath) as entries: + output = [{ + "name": it.name, + "path": os.path.join(filepath, it.name), + "is_dir": it.is_dir(), + "size": it.stat().st_size, + "atime": int(it.stat().st_atime), + "mtime": int(it.stat().st_mtime), + } for it in entries] + + if params['action'] == 'remove': + if os.path.isfile(filepath): + output = os.remove(filepath) + elif os.path.isdir(filepath): + output = os.removedirs(filename) + + if params['action'] == 'rename': + if os.path.exists(filepath): + output = shutil.move(filepath, params['target']) + + if params['action'] == 'copy': + if os.path.exists(filepath): + output = shutil.copy2(filepath, params['target']) + + if params['action'] == 'isfile': + output = os.path.isfile(filepath) + + if params['action'] == 'isdir': + output = os.path.isdir(filepath) + + + case 'clipboard': + # 读文本 + if params['action'] == 'wait_for_text': + output = self.clipboard.wait_for_text() + + # 写文本 + elif params['action'] == 'set_text': + self.clipboard.set_text(params['value'], -1) + + # 写图片 + elif params['action'] == 'set_image': + image = params['value'] + # 前端传进来的值, 如果是路径的话, 直接读取 + if type(image) == str: + image = GdkPixbuf.Pixbuf.new_from_file(image) + else: + image = dict_to_pixbuf(image) + + self.clipboard.set_image(image) + self.clipboard.store() + + # 读图片 + elif params['action'] == 'wait_for_image': + output = self.clipboard.wait_for_image() + output = pixbuf_to_dict(output, 'noname.png') + + # 清除剪切板 + elif params['action'] == 'clear': + self.clipboard.clear() + + # 退出app + case 'quit': + self.emit('quit') + + # 读取图片, 返回图片像素数据 + case 'image': + filename = params['value'] + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + output = pixbuf_to_dict(pixbuf, filename) + + case 'monitor': + if params['action'] == 'get-all': + monitor_num = self.display.get_n_monitors() + monitors = [self.display.get_monitor(i) for i in range(monitor_num)] + output = [get_monitor_info(m) for m in monitors] + + elif params['action'] == 'get-primary': + monitor = self.display.get_primary_monitor() + output = get_monitor_info(monitor) + + + case 'keybinder': + keymap = params.get('value') + shortcut_callback = params.get('shortcut_callback') or '' + + + if params['action'] == 'register': + # 绑定之前, 先解绑, 避免被重复绑定 + if Keybinder: + Keybinder.unbind(keymap) + output = Keybinder.bind( + keymap, + lambda km : self.call_js(shortcut_callback) + ) + + elif params['action'] == 'unregister': + if Keybinder: + Keybinder.unbind(keymap) + output = True + + elif params['action'] == 'supported': + if Keybinder: + output = Keybinder.supported() + else: + output = False + + + case 'tray': + if params['action'] == 'create': + pass + + elif params['action'] == 'remove': + pass + + case 'window': + if params['action'] == 'fullscreen': + self.window.fullscreen() + + elif params['action'] == 'unfullscreen': + self.window.unfullscreen() + + elif params['action'] == 'maximize': + self.window.maximize() + + elif params['action'] == 'unmaximize': + self.window.unmaximize() + + elif params['action'] == 'set_title': + self.window.set_title(params['value'] or '') + + elif params['action'] == 'resize': + self.window.resize(params['value'].get('width'), params['value'].get('height')) + + elif params['action'] == 'set_opacity': + self.window.set_opacity(params['value']) + + elif params['action'] == 'set_keep_above': + self.window.set_keep_above(params['value']) + + elif params['action'] == 'set_keep_below': + self.window.set_keep_below(params['value']) + + elif params['action'] == 'move': + self.window.move(params['value'].get('x'), params['value'].get('y')) + + elif params['action'] == 'toggle_visible': + if self.is_visible(): + self.window.hide() + else: + self.window.present() + + elif params['action'] == 'hide': + self.window.hide() + + elif params['action'] == 'show': + self.window.present() + + elif params['action'] == 'is_visible': + output = self.window.is_visible() + + + case 'notify': + title = params.get('title') + summary = params.get('summary') + icon = params.get('icon') + progress = params.get('progress') + urgency = params.get('urgency') + + self.notify.create(title, summary, icon, progress, urgency, params.get('callback')) + + case 'md5': + output = hashlib.md5(str(params.get('value'))).hexdigest() + + # 有回调则返回结果 + if callback: + self.call_js(callback, output) + \ No newline at end of file diff --git a/usr/lib/python3/dist-packages/webengine/gtk3/inject.js b/usr/lib/python3/dist-packages/webengine/gtk3/inject.js new file mode 100644 index 0000000..91ab9c4 --- /dev/null +++ b/usr/lib/python3/dist-packages/webengine/gtk3/inject.js @@ -0,0 +1,391 @@ +/** + * {注入的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 }) + } + }, + 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 + ) + }, + + md5(value = '') { + return handler('md5', { value }) + }, + + handler +})