Compare commits
No commits in common. "master" and "0.2.0" have entirely different histories.
|
@ -1,3 +1,2 @@
|
|||
__pycache__
|
||||
unpack
|
||||
*.deb
|
|
@ -1,5 +1,3 @@
|
|||
# python3-webengine-gtk3
|
||||
|
||||
基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力.
|
||||
|
||||
提供类似于electron的API和功能。
|
||||
基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力.
|
12
build.sh
12
build.sh
|
@ -1,13 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
version=$(python3 -c "import sys; sys.path.insert(0, './usr/lib/python3/dist-packages/');from webengine.gtk3 import version;print(version)")
|
||||
|
||||
|
||||
if [ -d unpack ]; then
|
||||
sudo rm -rf unpack
|
||||
fi
|
||||
|
||||
find usr -type d -name __pycache__ | xargs rm -rf
|
||||
version="0.2.0"
|
||||
|
||||
mkdir -p unpack/DEBIAN
|
||||
|
||||
|
@ -15,14 +12,9 @@ cp debian/control unpack/DEBIAN/
|
|||
cp -r usr unpack/
|
||||
|
||||
cd unpack
|
||||
find usr -type f | xargs md5sum > DEBIAN/md5sums
|
||||
|
||||
_size=$(du -d 0 usr | cut -f1)
|
||||
sed -i "s/{{size}}/${_size}/" DEBIAN/control
|
||||
sed -i "s/{{version}}/${version}/" DEBIAN/control
|
||||
find . -type f | xargs md5sum > DEBIAN/md5sums
|
||||
|
||||
cd ..
|
||||
sudo chown -R root:root unpack/
|
||||
dpkg-deb -b unpack/ "python3-webengine-gtk3-${version}.deb"
|
||||
|
||||
sudo rm -rf unpack
|
|
@ -1,12 +1,12 @@
|
|||
Package: python3-webengine-gtk3
|
||||
Version: {{version}}
|
||||
Version: 0.2.0
|
||||
Section: develop
|
||||
Priority: optional
|
||||
Maintainer: Yutent <yutent.io@gmail.com>
|
||||
Architecture: all
|
||||
Depends: python3 (>=3.10), python3-gi, gir1.2-webkit2-4.1, gir1.2-gtk-3.0, python3-pil, python3-gi-cairo, gir1.2-gdkpixbuf-2.0
|
||||
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: {{size}}
|
||||
Installed-Size: 324
|
||||
Homepage: https://git.wkit.fun/appcat/python3-webengine-gtk3
|
||||
Description: 一个傻瓜式的webview定制库.
|
||||
基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力.
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
# @author yutent<yutent.io@gmail.com>
|
||||
# @date 2023/08/31 11:55:25
|
||||
|
||||
from webengine.gtk3._webengine import WebEngine
|
||||
from webengine.gtk3._settings import create_setting
|
||||
from webengine.gtk3._hotreload import create_hmr_server
|
||||
|
||||
from ._version import *
|
||||
|
||||
from ._webengine import WebEngine
|
||||
from ._settings import create_setting
|
||||
from ._hotreload import create_hmr_server
|
||||
from ._custom_bridge import create_bridge
|
||||
build = (0, 2, 0)
|
||||
version = '.'.join(map(str, build))
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
def create_bridge():
|
||||
def wrapper(app, extra = None):
|
||||
app.custom_bridge = extra
|
||||
|
||||
return wrapper
|
|
@ -1,67 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
from gi.repository import Gtk, WebKit2
|
||||
|
||||
|
||||
def create_same_window(origin, req):
|
||||
WebEngine = type(origin)
|
||||
w, h = origin.window.get_size()
|
||||
win = Gtk.Window()
|
||||
win.set_default_size(w, h)
|
||||
|
||||
web = WebEngine(win, origin)
|
||||
web.set_zoom_level(origin.get_zoom_level())
|
||||
web.set_settings(origin.get_settings())
|
||||
web.load_request(req)
|
||||
|
||||
win.add(web)
|
||||
win.show_all()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def create_custom_window(origin, options):
|
||||
WebEngine = type(origin)
|
||||
_w, _h = origin.window.get_size()
|
||||
w = options.get('width') or _w
|
||||
h = options.get('height') or _h
|
||||
win = Gtk.Window()
|
||||
win.set_default_size(w, h)
|
||||
|
||||
wmclass = options.get('wmclass') or 'WebEngine Window'
|
||||
win.set_wmclass(wmclass, 'WebEngine')
|
||||
win.set_title(options.get('title') or 'WebEngine')
|
||||
|
||||
if options.get('icon_path'):
|
||||
win.set_icon_from_file(options['icon_path'])
|
||||
|
||||
if options.get('icon'):
|
||||
win.set_icon_name(options['icon'])
|
||||
|
||||
if options.get('frame') == False:
|
||||
win.set_decorated(False)
|
||||
|
||||
if options.get('x') is not None and options.get('y') is not None:
|
||||
win.move(options.get('x'), options.get('y'))
|
||||
|
||||
if options.get('always_on_top') == True:
|
||||
win.set_keep_above(options['always_on_top'])
|
||||
|
||||
if options.get('resizable') == False:
|
||||
win.set_resizable(False)
|
||||
|
||||
|
||||
web = WebEngine(win, origin, options.get('uuid'))
|
||||
web.set_root(origin.root, True)
|
||||
web.set_zoom_level(origin.get_zoom_level())
|
||||
web.set_settings(origin.get_settings())
|
||||
web.load(options.get('url'))
|
||||
|
||||
win.add(web)
|
||||
win.show_all()
|
||||
return web
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
import os
|
||||
|
||||
home_dir = os.getenv('HOME')
|
||||
|
||||
env = {
|
||||
"HOME_DIR": home_dir,
|
||||
"CONFIG_DIR": os.path.join(home_dir, '.config'),
|
||||
"CACHE_DIR": os.path.join(home_dir, '.cache'),
|
||||
"LANG": os.environ['LANG'] or "en_US.UTF-8"
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
# @date 2023/08/08 15:00:27
|
||||
|
||||
|
||||
import os, gi, json
|
||||
import os, gi
|
||||
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
|
||||
|
@ -11,20 +11,14 @@ from gi.repository import WebKit2
|
|||
|
||||
|
||||
class Inject:
|
||||
def __init__(self, webview, env = {}):
|
||||
def __init__(self, webview):
|
||||
|
||||
self.manager = webview.get_user_content_manager()
|
||||
|
||||
code = open(self.abspath('./inject.js'), 'r').read()
|
||||
script = open(self.abspath('./inject.js'), 'r').read()
|
||||
frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES
|
||||
time = WebKit2.UserScriptInjectionTime.START
|
||||
|
||||
code = code.replace("'{{env}}'", json.dumps(env))
|
||||
code = code.replace("{{uuid}}", webview.uuid)
|
||||
code = code.replace("{{app_name}}", webview.app_name)
|
||||
code = code.replace("{{app_version}}", webview.app_version)
|
||||
|
||||
script = WebKit2.UserScript(code, frame, time, None, None)
|
||||
time = WebKit2.UserScriptInjectionTime.END
|
||||
script = WebKit2.UserScript(script, frame, time, None, None)
|
||||
|
||||
self.manager.add_script(script)
|
||||
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
|
||||
import gi, os
|
||||
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
import os
|
||||
from gi.repository import Gio, WebKit2
|
||||
|
||||
from ._mimetypes import get_mimetype
|
||||
from ._version import version
|
||||
|
||||
__dir__ = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
class Protocal:
|
||||
root = ''
|
||||
|
@ -27,61 +22,29 @@ class Protocal:
|
|||
return os.path.join(self.root, filepath)
|
||||
|
||||
|
||||
def _get_error_page(self, tips = '404 not found!'):
|
||||
data = f"""
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Page not found</title>
|
||||
<style>body {{text-align: center;-webkit-user-select:none}} cite {{font-size:12px}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Oops!</h1>
|
||||
<h2>{tips}</h2>
|
||||
<hr>
|
||||
<cite>WebEngine v{version}</cite>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return data
|
||||
|
||||
def handle_response(self, req):
|
||||
schema = req.get_scheme()
|
||||
pathname = req.get_path()[1:]
|
||||
ext = pathname.split('.')[-1]
|
||||
mimetype = get_mimetype(ext)
|
||||
|
||||
if pathname == ext:
|
||||
pathname = 'index.html'
|
||||
|
||||
# print('----------------------------------------')
|
||||
# print(req.get_uri(),schema, pathname, ext, mimetype)
|
||||
# print(req.get_uri(),schema, pathname, ext, get_mimetype(ext))
|
||||
# print('----------------------------------------')
|
||||
|
||||
if schema == self.protocal:
|
||||
filepath = self.abspath(pathname)
|
||||
if os.path.isfile(filepath):
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
data = f.read()
|
||||
except Exception:
|
||||
data = self._get_error_page('403 Forbidden!')
|
||||
else:
|
||||
data = self._get_error_page()
|
||||
|
||||
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(mimetype)
|
||||
# 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, mimetype)
|
||||
req.finish(data, -1, get_mimetype(ext))
|
||||
|
||||
|
||||
def create_protocal(root):
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
import gi, os
|
||||
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
from gi.repository import WebKit2
|
||||
|
||||
from ._version import version
|
||||
from gi.repository import WebKit2
|
||||
|
||||
|
||||
class Settings(WebKit2.Settings):
|
||||
|
@ -17,12 +16,13 @@ class Settings(WebKit2.Settings):
|
|||
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_useragent_with_app('WebEngine', version)
|
||||
self.set_user_agent_with_application_details('WebEngine', '0.2.0')
|
||||
|
||||
|
||||
# indexedDB 和 localStorage 和 离线缓存
|
||||
|
@ -43,9 +43,6 @@ class Settings(WebKit2.Settings):
|
|||
self.set_media_playback_allows_inline(True)
|
||||
|
||||
|
||||
def set_useragent_with_app(self, name, ver):
|
||||
self.set_user_agent_with_application_details(name, ver)
|
||||
|
||||
def set_useragent(self, str):
|
||||
self.set_user_agent(str)
|
||||
|
||||
|
@ -76,10 +73,6 @@ def create_setting(options = None):
|
|||
if options is not None:
|
||||
if options.get('devtools'):
|
||||
setting.enable_devtools()
|
||||
setting.mock_devices()
|
||||
|
||||
if options.get('app_name') and options.get('app_version'):
|
||||
setting.set_useragent_with_app(options['app_name'], options['app_version'])
|
||||
|
||||
if options.get('useragent'):
|
||||
setting.set_useragent(options.get('useragent'))
|
||||
|
@ -90,11 +83,7 @@ def create_setting(options = None):
|
|||
if options.get('disable_fullscreen'):
|
||||
setting.disable_fullscreen()
|
||||
|
||||
|
||||
def wrapper(app, extra = None):
|
||||
if options is not None:
|
||||
app.app_name = options.get('app_name')
|
||||
app.app_version = options.get('app_version')
|
||||
app.set_settings(setting)
|
||||
|
||||
return wrapper
|
|
@ -3,12 +3,8 @@
|
|||
# @author yutent<yutent.io@gmail.com>
|
||||
# @date 2023/07/28 14:39:33
|
||||
|
||||
import gi, threading
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GdkPixbuf, GObject
|
||||
|
||||
def noop():
|
||||
pass
|
||||
import gi
|
||||
from gi.repository.GdkPixbuf import Pixbuf
|
||||
|
||||
def get_monitor_info(monitor):
|
||||
return {
|
||||
|
@ -54,7 +50,7 @@ def pixbuf_to_dict(pixbuf, filename = ''):
|
|||
|
||||
def dict_to_pixbuf(data):
|
||||
if data:
|
||||
image = GdkPixbuf.Pixbuf.new_from_data(
|
||||
image = Pixbuf.new_from_data(
|
||||
data = bytes(data['bytes']),
|
||||
colorspace = data['colorspace'],
|
||||
has_alpha = data['has_alpha'],
|
||||
|
@ -66,34 +62,4 @@ def dict_to_pixbuf(data):
|
|||
else:
|
||||
image = None
|
||||
|
||||
return image
|
||||
|
||||
|
||||
# 定义一个异步修饰器, 用于在子线程中运行一些会阻塞主线程的任务
|
||||
def run_async(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
thread = threading.Thread(target=func, args=args, kwargs=kwargs)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
return thread
|
||||
return wrapper
|
||||
|
||||
# 类型js的settimeout的修饰器
|
||||
def set_timeout(timeout = 0.5):
|
||||
def decorator(callback):
|
||||
def wrapper(*args):
|
||||
t = threading.Timer(timeout, callback, args=args)
|
||||
t.start()
|
||||
return t
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# 定义一个修饰器, 用于将当前方法转到主线程中运行 (子线程中调用方法时)
|
||||
def idle(func):
|
||||
def wrapper(*args):
|
||||
GObject.idle_add(func, *args)
|
||||
return wrapper
|
||||
|
||||
|
||||
return image
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
build = (0, 7, 0)
|
||||
version = '.'.join(map(str, build))
|
|
@ -3,15 +3,12 @@
|
|||
# @date 2023/08/08 14:07:26
|
||||
|
||||
|
||||
import gi, os, sys, json
|
||||
import webbrowser, shutil, hashlib, random
|
||||
import gi, os, json, shutil, hashlib
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
|
||||
|
||||
from gi.repository import GObject, Gtk, Gdk, WebKit2, GLib, Gio, GdkPixbuf
|
||||
from ._custom_window import create_same_window, create_custom_window
|
||||
|
||||
|
||||
# 优先尝试使用指示器, 没有再使用 Gtk.StatusIcon
|
||||
|
@ -32,50 +29,35 @@ try:
|
|||
except:
|
||||
Keybinder = None
|
||||
|
||||
from ._env import env
|
||||
from ._version import version
|
||||
|
||||
|
||||
|
||||
|
||||
from ._settings import create_setting
|
||||
from ._protocal import create_protocal
|
||||
from ._notify import create_notify
|
||||
from ._inject import Inject
|
||||
from ._utils import noop, get_monitor_info, pixbuf_to_dict, dict_to_pixbuf, run_async, idle
|
||||
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, ())
|
||||
}
|
||||
app_name = 'WebEngine'
|
||||
app_version = version
|
||||
uuid = None
|
||||
|
||||
root = None
|
||||
window = None
|
||||
opener = None
|
||||
children = set()
|
||||
custom_bridge = None
|
||||
|
||||
def __init__(self, win, opener = None, uuid = None):
|
||||
def __init__(self, window):
|
||||
|
||||
WebKit2.WebView.__init__(self)
|
||||
|
||||
if uuid is None:
|
||||
self.uuid = random.randbytes(8).hex()
|
||||
else:
|
||||
self.uuid = uuid
|
||||
|
||||
self.opener = opener
|
||||
self.window = win
|
||||
|
||||
if opener is not None:
|
||||
opener.children.add(self)
|
||||
|
||||
if win.get_title() is None:
|
||||
win.set_title(self.app_name)
|
||||
|
||||
if win.get_icon() is None and win.get_icon_name() is None:
|
||||
win.set_icon_name('web-browser')
|
||||
self.window = window
|
||||
|
||||
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
self.display = Gdk.Display.get_default()
|
||||
|
@ -87,82 +69,46 @@ class WebEngine(WebKit2.WebView):
|
|||
|
||||
|
||||
im = self.get_input_method_context()
|
||||
im.set_enable_preedit(True)
|
||||
|
||||
# 解决输入法候选框跟随问题
|
||||
im.set_enable_preedit(False)
|
||||
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, env).connect(self.called_by_js)
|
||||
|
||||
self.connect('create', self.create_new_window)
|
||||
|
||||
# 允许前端 widnow.close() 关闭窗口
|
||||
self.connect('close', self.close_window)
|
||||
|
||||
# 通过外部关闭窗口时从父级中移除
|
||||
win.connect("destroy", self.remove_from_opener)
|
||||
win.connect('hide', lambda w: self.call_js('hide'))
|
||||
win.connect('show', lambda w: self.call_js('show'))
|
||||
Inject(self).connect(self.called_by_js)
|
||||
|
||||
|
||||
def remove_from_opener(self, win = None):
|
||||
if self.opener is not None:
|
||||
try:
|
||||
self.opener.children.remove(self)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def close_window(self, wv = None):
|
||||
self.remove_from_opener()
|
||||
self.window.close()
|
||||
|
||||
|
||||
# 响应原生js的 window.open
|
||||
def create_new_window(self, webview, nav):
|
||||
req = nav.get_request()
|
||||
return create_same_window(self, req)
|
||||
|
||||
|
||||
# 手动创建窗口
|
||||
def new_window_by_custom(self, options = {}):
|
||||
return create_custom_window(self, options)
|
||||
|
||||
|
||||
def set_root(self, root, has_protocol = False):
|
||||
def set_root(self, root):
|
||||
self.root = root
|
||||
|
||||
if has_protocol:
|
||||
return self
|
||||
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 url.startswith('http://') or url.startswith('https://'):
|
||||
self.load_uri(url)
|
||||
if self.root is None:
|
||||
raise EnvironmentError('web root dir not set!')
|
||||
else:
|
||||
if self.root is None:
|
||||
raise EnvironmentError('web root dir not set!')
|
||||
else:
|
||||
if url.startswith('/'):
|
||||
self.load_uri(f"app://{url}")
|
||||
elif url.startswith('app://'):
|
||||
self.load_uri(url)
|
||||
else:
|
||||
raise ValueError('url must starts with "/" or "app://"')
|
||||
self.load_uri(f"app://{url}")
|
||||
|
||||
|
||||
@idle
|
||||
def call_js(self, method, data = None, err = None):
|
||||
if err is not None:
|
||||
err = str(err)
|
||||
scripts = 'native.$emit("' + method + '", ' + json.dumps(err) + ', ' + json.dumps(data) + ')'
|
||||
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)
|
||||
|
||||
@run_async
|
||||
|
||||
def called_by_js(self, webview, message):
|
||||
|
||||
data = json.loads(message.get_js_value().to_json(0))
|
||||
|
@ -171,21 +117,98 @@ class WebEngine(WebKit2.WebView):
|
|||
callback = data.get('callback')
|
||||
params = data.get('data')
|
||||
output = None
|
||||
_error = None
|
||||
|
||||
|
||||
match event:
|
||||
|
||||
case 'app':
|
||||
_error, output = self._app_handler(params)
|
||||
|
||||
case 'fs':
|
||||
_error, output = self._fs_handler(params)
|
||||
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':
|
||||
_error, output = self._clipboard_handler(params)
|
||||
# 读文本
|
||||
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']
|
||||
|
@ -204,8 +227,30 @@ class WebEngine(WebKit2.WebView):
|
|||
|
||||
|
||||
case 'keybinder':
|
||||
_error, output = self._keybinder_handler(params)
|
||||
|
||||
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':
|
||||
|
@ -214,329 +259,66 @@ class WebEngine(WebKit2.WebView):
|
|||
elif params['action'] == 'remove':
|
||||
pass
|
||||
|
||||
case 'opener':
|
||||
callback = 'opener_message'
|
||||
output = params.get('data')
|
||||
uuid = json.dumps(self.uuid if self.opener else None)
|
||||
scripts = f"native.$emit('opener_message', {json.dumps(output)}, {uuid})"
|
||||
if self.opener is None:
|
||||
self.evaluate_javascript(scripts, -1)
|
||||
else:
|
||||
self.opener.evaluate_javascript(scripts, -1)
|
||||
return
|
||||
|
||||
case 'children':
|
||||
callback = 'opener_message'
|
||||
output = params.get('data')
|
||||
uuid = params.get('uuid')
|
||||
scripts = f"native.$emit('opener_message', {json.dumps(output)})"
|
||||
if len(self.children) == 0:
|
||||
self.evaluate_javascript(scripts, -1)
|
||||
else:
|
||||
if uuid is None:
|
||||
for child in self.children:
|
||||
child.evaluate_javascript(scripts, -1)
|
||||
else:
|
||||
for child in self.children:
|
||||
if child.uuid == uuid:
|
||||
child.evaluate_javascript(scripts, -1)
|
||||
break
|
||||
return
|
||||
|
||||
case 'window':
|
||||
_error, output = self._window_handler(params)
|
||||
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':
|
||||
if self.notify is None:
|
||||
_error = ImportError('Notify module not found. Need to install gir1.2-notify-0.7 if you use debian.')
|
||||
else:
|
||||
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 'proxy':
|
||||
_error, output = self._proxy_handler(params)
|
||||
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()
|
||||
|
||||
case _:
|
||||
if self.custom_bridge is not None:
|
||||
try:
|
||||
_error, output = self.custom_bridge(event, params) or (None, None)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
_error = err
|
||||
|
||||
# 有回调则返回结果
|
||||
if callback:
|
||||
self.call_js(callback, output, _error)
|
||||
|
||||
|
||||
def _app_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
match(params.get('action')):
|
||||
# 退出app
|
||||
case 'quit':
|
||||
self.close_window()
|
||||
self.emit('quit')
|
||||
|
||||
case 'relaunch':
|
||||
py = sys.executable
|
||||
os.execl(py, py, *sys.argv)
|
||||
|
||||
return (_error, output)
|
||||
|
||||
def _shell_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
path = params.get('path')
|
||||
|
||||
match(params.get('action')):
|
||||
case 'openExternal':
|
||||
webbrowser.open(params.get('url'))
|
||||
|
||||
case 'showItemInFolder':
|
||||
os.system(f"xdg-open '{path}'")
|
||||
|
||||
case 'openPath':
|
||||
os.system(f"xdg-open '{path}'")
|
||||
|
||||
case 'trashItem':
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def _fs_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
filepath = params.get('filepath')
|
||||
|
||||
match(params.get('action')):
|
||||
case 'access':
|
||||
try:
|
||||
file = open(filepath, params.get('mode'))
|
||||
file.close()
|
||||
output = True
|
||||
except Exception as err:
|
||||
output = False
|
||||
|
||||
case 'read':
|
||||
try:
|
||||
with open(filepath, params.get('mode')) as file:
|
||||
output = file.read()
|
||||
if params.get('mode').find('b') > -1:
|
||||
output = list(output)
|
||||
except Exception as err:
|
||||
_error = err
|
||||
|
||||
case 'write':
|
||||
# 调整以支持二进制数据写入
|
||||
try:
|
||||
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)
|
||||
except Exception as err:
|
||||
_error = err
|
||||
|
||||
case 'exists':
|
||||
output = os.path.exists(filepath)
|
||||
|
||||
case '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]
|
||||
|
||||
case 'remove':
|
||||
if os.path.isfile(filepath):
|
||||
output = os.remove(filepath)
|
||||
elif os.path.isdir(filepath):
|
||||
output = os.removedirs(filepath)
|
||||
|
||||
case 'rename':
|
||||
if os.path.exists(filepath):
|
||||
output = shutil.move(filepath, params['target'])
|
||||
|
||||
case 'copy':
|
||||
if os.path.exists(filepath):
|
||||
output = shutil.copy2(filepath, params['target'])
|
||||
|
||||
case 'isfile':
|
||||
output = os.path.isfile(filepath)
|
||||
|
||||
case 'isdir':
|
||||
output = os.path.isdir(filepath)
|
||||
|
||||
case 'mkdir':
|
||||
output = os.makedirs(filepath)
|
||||
|
||||
return (_error, output)
|
||||
|
||||
|
||||
|
||||
def _clipboard_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
|
||||
match(params.get('action')):
|
||||
# 读文本
|
||||
case 'wait_for_text':
|
||||
output = self.clipboard.wait_for_text()
|
||||
|
||||
# 写文本
|
||||
case 'set_text':
|
||||
self.clipboard.set_text(params['value'], -1)
|
||||
|
||||
# 写图片
|
||||
case '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()
|
||||
|
||||
# 读图片
|
||||
case 'wait_for_image':
|
||||
output = self.clipboard.wait_for_image()
|
||||
output = pixbuf_to_dict(output, 'noname.png')
|
||||
|
||||
# 清除剪切板
|
||||
case 'clear':
|
||||
self.clipboard.clear()
|
||||
|
||||
return (_error, output)
|
||||
|
||||
|
||||
def _keybinder_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
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
|
||||
|
||||
return (_error, output)
|
||||
|
||||
|
||||
|
||||
def _window_handler(self, params = {}):
|
||||
_error = None
|
||||
output = None
|
||||
|
||||
match(params.get('action')):
|
||||
|
||||
case 'create':
|
||||
self.new_window_by_custom(params.get('options'))
|
||||
|
||||
case 'close':
|
||||
self.close_window()
|
||||
|
||||
case 'fullscreen':
|
||||
self.window.fullscreen()
|
||||
|
||||
case 'unfullscreen':
|
||||
self.window.unfullscreen()
|
||||
|
||||
case 'maximize':
|
||||
self.window.maximize()
|
||||
|
||||
case 'unmaximize':
|
||||
self.window.unmaximize()
|
||||
|
||||
case 'set_title':
|
||||
self.window.set_title(params['value'] or '')
|
||||
|
||||
case 'resize':
|
||||
self.window.resize(params['value'].get('width'), params['value'].get('height'))
|
||||
|
||||
case 'set_opacity':
|
||||
self.window.set_opacity(params['value'])
|
||||
|
||||
case 'set_keep_above':
|
||||
self.window.set_keep_above(params['value'])
|
||||
|
||||
case 'set_keep_below':
|
||||
self.window.set_keep_below(params['value'])
|
||||
|
||||
case 'move':
|
||||
self.window.move(params['value'].get('x'), params['value'].get('y'))
|
||||
|
||||
case 'toggle_visible':
|
||||
if self.window.is_visible():
|
||||
self.window.hide()
|
||||
else:
|
||||
self.window.present()
|
||||
|
||||
case 'hide':
|
||||
self.window.hide()
|
||||
|
||||
case 'show':
|
||||
self.window.present()
|
||||
|
||||
case 'is_visible':
|
||||
output = self.window.is_visible()
|
||||
|
||||
|
||||
return (_error, output)
|
||||
|
||||
|
||||
|
||||
def _proxy_handler(self, params = {}):
|
||||
dm = self.get_website_data_manager()
|
||||
output = True
|
||||
_error = None
|
||||
if params['action'] == 'disable':
|
||||
dm.set_network_proxy_settings(WebKit2.NetworkProxyMode.NO_PROXY, None)
|
||||
elif params['action'] == 'system':
|
||||
dm.set_network_proxy_settings(WebKit2.NetworkProxyMode.DEFAULT, None)
|
||||
else:
|
||||
try:
|
||||
proxy = WebKit2.NetworkProxySettings(params['url'], params['ignore'])
|
||||
dm.set_network_proxy_settings(WebKit2.NetworkProxyMode.CUSTOM, proxy)
|
||||
except Exception as err:
|
||||
_error = err
|
||||
output = False
|
||||
return (_error, output)
|
||||
|
||||
|
||||
self.call_js(callback, output)
|
||||
|
|
@ -3,509 +3,389 @@
|
|||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2023/07/21 17:38:11
|
||||
*/
|
||||
!(function () {
|
||||
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 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: '<Shift>',
|
||||
ctrl: '<Ctrl>',
|
||||
alt: '<Alt>',
|
||||
super: '<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)
|
||||
}
|
||||
const KEYS_MAP = {
|
||||
shift: '<Shift>',
|
||||
ctrl: '<Ctrl>',
|
||||
alt: '<Alt>',
|
||||
super: '<Super>'
|
||||
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()]
|
||||
}
|
||||
|
||||
const NO_CALLBACK = false
|
||||
const CALL_ONCE = true
|
||||
|
||||
const __events__ = Symbol('events')
|
||||
|
||||
function defer() {
|
||||
let obj = {}
|
||||
obj.promise = new Promise((resolve, reject) => {
|
||||
obj.resolve = resolve
|
||||
obj.reject = reject
|
||||
})
|
||||
return obj
|
||||
toJSON() {
|
||||
return this.#origin
|
||||
}
|
||||
|
||||
function rand(prefix = 'cb_') {
|
||||
return prefix + Math.random().toString().slice(2)
|
||||
}
|
||||
|
||||
function handler(event, data = {}, need = CALL_ONCE) {
|
||||
export(type, base64) {
|
||||
let _ = defer()
|
||||
let callback
|
||||
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
|
||||
|
||||
if (need === NO_CALLBACK) {
|
||||
_.resolve(true)
|
||||
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 {
|
||||
callback = rand()
|
||||
native.$once(callback, (err, res) => {
|
||||
if (err) {
|
||||
_.reject(new Error(err))
|
||||
} else {
|
||||
_.resolve(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.webkit.messageHandlers.app.postMessage({
|
||||
event,
|
||||
data,
|
||||
callback
|
||||
})
|
||||
return _.promise
|
||||
}
|
||||
|
||||
function base64(str = '') {
|
||||
return btoa(str).replace(/[+=\/]/g, '')
|
||||
}
|
||||
|
||||
function _postMessage(data = {}, uuid = null) {
|
||||
let ev = new Event('message')
|
||||
Object.assign(ev, {
|
||||
data,
|
||||
source: {
|
||||
postMessage(msg) {
|
||||
native.children.postMessage(msg, uuid)
|
||||
}
|
||||
}
|
||||
})
|
||||
window.dispatchEvent(ev)
|
||||
}
|
||||
|
||||
function readonly(obj, key, value) {
|
||||
Object.defineProperty(obj, key, {
|
||||
get() {
|
||||
return value
|
||||
},
|
||||
enumerable: false
|
||||
})
|
||||
}
|
||||
|
||||
function _extend(origin, options = {}) {
|
||||
for (let k in options) {
|
||||
readonly(origin, k, options[k])
|
||||
canvas.toBlob(_.resolve, type || this.type, 1)
|
||||
return _.promise
|
||||
}
|
||||
}
|
||||
|
||||
class NativeImage {
|
||||
#origin
|
||||
toPNG() {
|
||||
return this.export('image/png')
|
||||
}
|
||||
|
||||
constructor(obj) {
|
||||
this.#origin = obj
|
||||
this.width = obj.width
|
||||
this.height = obj.height
|
||||
this.type = MIME_TYPES[obj.filepath.split('.').pop()]
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.#origin
|
||||
}
|
||||
$once(name, fn) {
|
||||
fn.__once__ = true
|
||||
this.$on(name, fn)
|
||||
}
|
||||
|
||||
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)
|
||||
$off(name, fn) {
|
||||
if (this.__events__[name]) {
|
||||
if (fn) {
|
||||
this.__events__[name] = this.__events__[name].filter(it => it !== fn)
|
||||
} else {
|
||||
canvas.toBlob(_.resolve, type || this.type, 1)
|
||||
return _.promise
|
||||
this.__events__[name] = []
|
||||
}
|
||||
}
|
||||
|
||||
toPNG() {
|
||||
return this.export('image/png')
|
||||
}
|
||||
|
||||
toJPEG() {
|
||||
return this.export('image/jpeg')
|
||||
}
|
||||
|
||||
toDataURL(type) {
|
||||
return this.export(type, true)
|
||||
}
|
||||
}
|
||||
|
||||
class Native {
|
||||
//
|
||||
[__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)
|
||||
$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)
|
||||
}
|
||||
}
|
||||
|
||||
readonly(window, 'native', new Native())
|
||||
$destroy() {
|
||||
this.__events__ = Object.create(null)
|
||||
}
|
||||
}
|
||||
|
||||
native.$on('opener_message', (data, uuid) => _postMessage(data, uuid))
|
||||
window.native = new EventEmitter()
|
||||
|
||||
_extend(native, {
|
||||
env: '{{env}}',
|
||||
app: {
|
||||
name: '{{app_name}}',
|
||||
version: '{{app_version}}',
|
||||
quit() {
|
||||
return handler('app', { action: 'quit' }, NO_CALLBACK)
|
||||
},
|
||||
relaunch() {
|
||||
return handler('app', { action: 'relaunch' }, NO_CALLBACK)
|
||||
},
|
||||
getLocale() {
|
||||
return native.env.LANG
|
||||
}
|
||||
},
|
||||
shell: {
|
||||
openExternal(url) {},
|
||||
showItemInFolder(path) {},
|
||||
openPath(path) {},
|
||||
trashItem(path) {}
|
||||
},
|
||||
fs: {
|
||||
access(filepath, mode = 'r') {
|
||||
return handler('fs', { action: 'access', mode, filepath })
|
||||
},
|
||||
|
||||
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',
|
||||
mode,
|
||||
filepath,
|
||||
content
|
||||
})
|
||||
},
|
||||
append(filepath, content = '', mode = 'a+') {
|
||||
return handler('fs', {
|
||||
action: 'write',
|
||||
mode,
|
||||
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 })
|
||||
},
|
||||
mkdir(filepath) {
|
||||
return handler('fs', { action: 'mkdir', filepath })
|
||||
}
|
||||
},
|
||||
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 }, NO_CALLBACK)
|
||||
},
|
||||
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 }, NO_CALLBACK)
|
||||
},
|
||||
clear() {
|
||||
return handler('clipboard', { action: 'clear' }, NO_CALLBACK)
|
||||
}
|
||||
},
|
||||
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 <Number> 0: 隐藏, 1: 显示, 2: 重要(对应上面的attention_icon)
|
||||
*/
|
||||
set_status(status) {
|
||||
return handler('tray', { action: 'set_status', value: status })
|
||||
}
|
||||
},
|
||||
opener: {
|
||||
postMessage(data = {}) {
|
||||
return handler('opener', { action: 'postmessage', data }, NO_CALLBACK)
|
||||
}
|
||||
},
|
||||
children: {
|
||||
postMessage(data = {}, uuid = null) {
|
||||
return handler(
|
||||
'children',
|
||||
{ action: 'postmessage', data, uuid },
|
||||
NO_CALLBACK
|
||||
)
|
||||
}
|
||||
},
|
||||
window: {
|
||||
uuid: '{{uuid}}',
|
||||
create(options = {}) {
|
||||
return handler('window', { action: 'create', options })
|
||||
},
|
||||
|
||||
close() {
|
||||
return handler('window', { action: 'close' })
|
||||
},
|
||||
|
||||
isVisible() {
|
||||
return handler('window', { action: 'is_visible' })
|
||||
},
|
||||
toggleVisible() {
|
||||
handler('window', { action: 'toggle_visible' }, NO_CALLBACK)
|
||||
},
|
||||
hide() {
|
||||
handler('window', { action: 'hide' }, NO_CALLBACK)
|
||||
},
|
||||
show() {
|
||||
handler('window', { action: 'show' }, NO_CALLBACK)
|
||||
},
|
||||
fullscreen() {
|
||||
handler('window', { action: 'fullscreen' }, NO_CALLBACK)
|
||||
},
|
||||
unfullscreen() {
|
||||
handler('window', { action: 'unfullscreen' }, NO_CALLBACK)
|
||||
},
|
||||
maximize() {
|
||||
handler('window', { action: 'maximize' }, NO_CALLBACK)
|
||||
},
|
||||
unmaximize() {
|
||||
handler('window', { action: 'unmaximize' }, NO_CALLBACK)
|
||||
},
|
||||
setTitle(title = '') {
|
||||
handler('window', { action: 'set_title', value: title }, NO_CALLBACK)
|
||||
},
|
||||
resize(width = 0, height = 0) {
|
||||
handler(
|
||||
'window',
|
||||
{ action: 'resize', value: { width, height } },
|
||||
NO_CALLBACK
|
||||
)
|
||||
},
|
||||
move(x = 0, y = 0) {
|
||||
handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK)
|
||||
},
|
||||
setOpacity(opacity = 1) {
|
||||
handler(
|
||||
'window',
|
||||
{ action: 'set_opacity', value: opacity },
|
||||
NO_CALLBACK
|
||||
)
|
||||
},
|
||||
alwayOnTop(setting = true) {
|
||||
handler(
|
||||
'window',
|
||||
{ action: 'set_keep_above', value: setting },
|
||||
NO_CALLBACK
|
||||
)
|
||||
},
|
||||
alwayOnBotttom(setting = true) {
|
||||
handler(
|
||||
'window',
|
||||
{ action: 'set_keep_below', value: setting },
|
||||
NO_CALLBACK
|
||||
)
|
||||
}
|
||||
},
|
||||
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 },
|
||||
NO_CALLBACK
|
||||
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
|
||||
)
|
||||
},
|
||||
|
||||
md5(value = '') {
|
||||
return handler('md5', { value })
|
||||
write(filepath, content = '', mode = 'w') {
|
||||
return handler('fs', {
|
||||
action: 'write',
|
||||
append: false,
|
||||
filepath,
|
||||
content
|
||||
})
|
||||
},
|
||||
|
||||
proxy: {
|
||||
disable() {
|
||||
return handler('proxy', { action: 'disable' }, NO_CALLBACK)
|
||||
},
|
||||
system() {
|
||||
return handler('proxy', { action: 'system' }, NO_CALLBACK)
|
||||
},
|
||||
custom(url = '', ignore = null) {
|
||||
return handler('proxy', { action: 'enable', url, ignore }, NO_CALLBACK)
|
||||
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() {
|
||||
//
|
||||
},
|
||||
|
||||
handler
|
||||
})
|
||||
})()
|
||||
/**
|
||||
* 设置普通状态的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 <Number> 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
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue