Compare commits

...

32 Commits

Author SHA1 Message Date
yutent 64dc70b594 js调用python改为异步; js调用python方法异常信息改为Error对象;优化自定义桥接方法的异常处理 2024-01-24 17:37:31 +08:00
yutent 3062354338 js注入改为开始时注入, 窗口增加hide和show 2个回调 2023-09-13 14:04:51 +08:00
yutent ece744aafe 增加shell操作 2023-09-12 18:55:32 +08:00
yutent b398551deb native对象及属性改为只读 2023-09-12 14:28:04 +08:00
yutent 5da22eed8e 移除locale的注入及navigator.language的重写; native对象改为只读 2023-09-12 14:15:27 +08:00
yutent f9c4007e3f 调整环境变量 2023-09-12 14:12:29 +08:00
yutent 2682af069b 修复打包 2023-09-11 14:31:18 +08:00
yutent 838d9d1134 修复依赖缺失 2023-09-11 13:22:53 +08:00
yutent c17d171ad8 增加app_name和app_version的设置及注入 2023-09-08 18:55:49 +08:00
yutent b9e7144271 quit方法移入app对象; app新增relaunch方法 2023-09-08 18:38:39 +08:00
yutent 6e0beb6a66 跳出循环 2023-09-08 17:50:10 +08:00
yutent 89717b4019 0.6.0 2023-09-08 17:25:11 +08:00
yutent 6f7141560a 窗口增加uuid属性; 新建窗口增加icon支持;增加父窗口向子窗口发送消息 2023-09-08 17:22:59 +08:00
yutent 4ef23a703d 移除多余的引入 2023-09-08 16:03:09 +08:00
yutent c1f7e8e503 0.5.0 2023-09-08 15:09:22 +08:00
yutent 80872e0678 使用python在多窗口之间做桥接 2023-09-08 15:06:42 +08:00
yutent 1f2273150c 修复窗口移动API; 增加新窗口打开的支持 2023-09-07 19:58:17 +08:00
yutent ed5dcf1245 增加403,404页面; 支持前端history路由 2023-09-07 11:39:56 +08:00
yutent d499991831 优化打包脚本; load方法支持http远程地址 2023-09-07 10:41:42 +08:00
yutent c885d72b8f 新开窗口同步缩放系数 2023-09-06 17:29:35 +08:00
yutent 8822c8effd 修复fs模式错误; 增加window.open的支持 2023-09-06 17:02:37 +08:00
yutent fe56af2292 完成自定义桥接功能 2023-09-06 15:30:09 +08:00
yutent 1ba77b4cce 修复gtk3版本声明问题; 版本号改为动态读取 2023-09-06 14:47:56 +08:00
yutent 5424d3ff74 修复fs.remove一处书写错误; 优化js交互 2023-09-06 14:16:57 +08:00
yutent 80d486e87b 修复输入法跟随; 增加代理设置API 2023-09-06 12:21:56 +08:00
yutent d2f08af833 update 2023-09-05 18:49:32 +08:00
yutent 5b96a9a291 fixed 2023-09-05 16:54:00 +08:00
yutent ebe9eacd10 更新打包配置 2023-09-05 14:56:01 +08:00
yutent b2153978c9 调整配置 2023-09-05 14:18:25 +08:00
yutent 8abfbee5dc 调整初始化方式 2023-09-04 19:21:05 +08:00
yutent 2aaa711a89 优化注入, 支持异常输出 2023-09-04 19:11:36 +08:00
yutent 2ddfa5e2ab 修复js注入; 增加ENV注入 2023-09-04 18:29:12 +08:00
15 changed files with 1106 additions and 578 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__
unpack
*.deb

View File

@ -1,3 +1,5 @@
# python3-webengine-gtk3
基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力.
提供类似于electron的API和功能。

View File

@ -1,10 +1,13 @@
#!/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
version="0.2.0"
find usr -type d -name __pycache__ | xargs rm -rf
mkdir -p unpack/DEBIAN
@ -12,9 +15,14 @@ cp debian/control unpack/DEBIAN/
cp -r usr unpack/
cd unpack
find . -type f | xargs md5sum > DEBIAN/md5sums
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
cd ..
sudo chown -R root:root unpack/
dpkg-deb -b unpack/ "python3-webengine-gtk3-${version}.deb"
sudo rm -rf unpack

6
debian/control vendored
View File

@ -1,12 +1,12 @@
Package: python3-webengine-gtk3
Version: 0.2.0
Version: {{version}}
Section: develop
Priority: optional
Maintainer: Yutent <yutent.io@gmail.com>
Architecture: all
Depends: python3 (>=3.10), python3-gi, gir1.2-gtk-3.0, python3-pil, python3-gi-cairo, gir1.2-gdkpixbuf-2.0
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
Recommends: gir1.2-ayatanaappindicator3-0.1, gir1.2-notify-0.7, gir1.2-keybinder-3.0
Installed-Size: 324
Installed-Size: {{size}}
Homepage: https://git.wkit.fun/appcat/python3-webengine-gtk3
Description: 一个傻瓜式的webview定制库.
基于webkit2封装的webview库, 提供傻瓜式的定制, 和一系列的js方法的注入, 增加前端js直接与系统交互的能力.

View File

@ -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 *
build = (0, 2, 0)
version = '.'.join(map(str, build))
from ._webengine import WebEngine
from ._settings import create_setting
from ._hotreload import create_hmr_server
from ._custom_bridge import create_bridge

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python3
def create_bridge():
def wrapper(app, extra = None):
app.custom_bridge = extra
return wrapper

View File

@ -0,0 +1,67 @@
#!/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

View File

@ -0,0 +1,13 @@
#!/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"
}

View File

@ -3,7 +3,7 @@
# @date 2023/08/08 15:00:27
import os, gi
import os, gi, json
gi.require_version("WebKit2", "4.1")
@ -11,14 +11,20 @@ from gi.repository import WebKit2
class Inject:
def __init__(self, webview):
def __init__(self, webview, env = {}):
self.manager = webview.get_user_content_manager()
script = open(self.abspath('./inject.js'), 'r').read()
code = open(self.abspath('./inject.js'), 'r').read()
frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES
time = WebKit2.UserScriptInjectionTime.END
script = WebKit2.UserScript(script, frame, time, None, None)
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)
self.manager.add_script(script)

View File

@ -1,8 +1,13 @@
import os
import gi, os
gi.require_version("WebKit2", "4.1")
from gi.repository import Gio, WebKit2
from ._mimetypes import get_mimetype
from ._mimetypes import get_mimetype
from ._version import version
__dir__ = os.path.dirname(os.path.realpath(__file__))
class Protocal:
root = ''
@ -22,29 +27,61 @@ 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, get_mimetype(ext))
# print(req.get_uri(),schema, pathname, ext, mimetype)
# print('----------------------------------------')
if schema == self.protocal:
data = open(self.abspath(pathname)).read()
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 = Gio.MemoryInputStream.new_from_data(data.encode())
# ------- 更多功能的reponse ----------------
# res = WebKit2.URISchemeResponse.new(data, -1)
# res.set_content_type(get_mimetype(ext))
# res.set_content_type(mimetype)
# res.set_http_headers('text/html')
# res.set_status(200)
# req.finish_with_response(res)
# ----------------------------------------
# 简单的response
req.finish(data, -1, get_mimetype(ext))
req.finish(data, -1, mimetype)
def create_protocal(root):

View File

@ -6,9 +6,10 @@
import gi, os
gi.require_version("WebKit2", "4.1")
from gi.repository import WebKit2
from ._version import version
class Settings(WebKit2.Settings):
def __init__(self):
@ -16,13 +17,12 @@ 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_user_agent_with_application_details('WebEngine', '0.2.0')
self.set_useragent_with_app('WebEngine', version)
# indexedDB 和 localStorage 和 离线缓存
@ -43,6 +43,9 @@ 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)
@ -73,6 +76,10 @@ 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'))
@ -83,7 +90,11 @@ 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

View File

@ -3,8 +3,12 @@
# @author yutent<yutent.io@gmail.com>
# @date 2023/07/28 14:39:33
import gi
from gi.repository.GdkPixbuf import Pixbuf
import gi, threading
gi.require_version('Gtk', '3.0')
from gi.repository import GdkPixbuf, GObject
def noop():
pass
def get_monitor_info(monitor):
return {
@ -50,7 +54,7 @@ def pixbuf_to_dict(pixbuf, filename = ''):
def dict_to_pixbuf(data):
if data:
image = Pixbuf.new_from_data(
image = GdkPixbuf.Pixbuf.new_from_data(
data = bytes(data['bytes']),
colorspace = data['colorspace'],
has_alpha = data['has_alpha'],
@ -63,3 +67,33 @@ def dict_to_pixbuf(data):
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

View File

@ -0,0 +1,4 @@
#!/usr/bin/env python3
build = (0, 7, 0)
version = '.'.join(map(str, build))

View File

@ -3,12 +3,15 @@
# @date 2023/08/08 14:07:26
import gi, os, json, shutil, hashlib
import gi, os, sys, json
import webbrowser, shutil, hashlib, random
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
@ -29,35 +32,50 @@ 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 get_monitor_info, pixbuf_to_dict, dict_to_pixbuf
from ._utils import noop, get_monitor_info, pixbuf_to_dict, dict_to_pixbuf, run_async, idle
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, window):
def __init__(self, win, opener = None, uuid = None):
WebKit2.WebView.__init__(self)
self.window = window
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.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.display = Gdk.Display.get_default()
@ -69,46 +87,82 @@ class WebEngine(WebKit2.WebView):
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)
im.set_enable_preedit(False)
Inject(self).connect(self.called_by_js)
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'))
def set_root(self, root):
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):
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)
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://"')
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) + ')'
@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) + ')'
self.evaluate_javascript(scripts, -1)
@run_async
def called_by_js(self, webview, message):
data = json.loads(message.get_js_value().to_json(0))
@ -117,97 +171,20 @@ 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':
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)
_error, output = self._fs_handler(params)
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')
_error, output = self._clipboard_handler(params)
# 读取图片, 返回图片像素数据
case 'image':
@ -227,6 +204,234 @@ class WebEngine(WebKit2.WebView):
case 'keybinder':
_error, output = self._keybinder_handler(params)
case 'tray':
if params['action'] == 'create':
pass
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)
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)
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 ''
@ -251,74 +456,87 @@ class WebEngine(WebKit2.WebView):
else:
output = False
return (_error, output)
case 'tray':
if params['action'] == 'create':
pass
elif params['action'] == 'remove':
pass
case 'window':
if params['action'] == 'fullscreen':
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()
elif params['action'] == 'unfullscreen':
case 'unfullscreen':
self.window.unfullscreen()
elif params['action'] == 'maximize':
case 'maximize':
self.window.maximize()
elif params['action'] == 'unmaximize':
case 'unmaximize':
self.window.unmaximize()
elif params['action'] == 'set_title':
case 'set_title':
self.window.set_title(params['value'] or '')
elif params['action'] == 'resize':
case 'resize':
self.window.resize(params['value'].get('width'), params['value'].get('height'))
elif params['action'] == 'set_opacity':
case 'set_opacity':
self.window.set_opacity(params['value'])
elif params['action'] == 'set_keep_above':
case 'set_keep_above':
self.window.set_keep_above(params['value'])
elif params['action'] == 'set_keep_below':
case 'set_keep_below':
self.window.set_keep_below(params['value'])
elif params['action'] == 'move':
case 'move':
self.window.move(params['value'].get('x'), params['value'].get('y'))
elif params['action'] == 'toggle_visible':
if self.is_visible():
case 'toggle_visible':
if self.window.is_visible():
self.window.hide()
else:
self.window.present()
elif params['action'] == 'hide':
case 'hide':
self.window.hide()
elif params['action'] == 'show':
case 'show':
self.window.present()
elif params['action'] == 'is_visible':
case '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')
return (_error, output)
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)
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)

View File

@ -3,8 +3,8 @@
* @author yutent<yutent.io@gmail.com>
* @date 2023/07/21 17:38:11
*/
const MIME_TYPES = {
!(function () {
const MIME_TYPES = {
html: 'text/html',
json: 'application/json',
js: 'application/javascript',
@ -37,50 +37,90 @@ const MIME_TYPES = {
doc: 'application/msword',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
const KEYS_MAP = {
}
const KEYS_MAP = {
shift: '<Shift>',
ctrl: '<Ctrl>',
alt: '<Alt>',
super: '<Super>'
}
}
function defer() {
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
}
}
function rand(prefix = 'cb_') {
function rand(prefix = 'cb_') {
return prefix + Math.random().toString().slice(2)
}
}
function handler(event, data = {}, once = true) {
function handler(event, data = {}, need = CALL_ONCE) {
let _ = defer()
let callback
if (typeof once === 'boolean') {
callback = rand()
native[once ? '$once' : '$on'](callback, _.resolve)
} else {
if (need === NO_CALLBACK) {
_.resolve(true)
} 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 = '') {
function base64(str = '') {
return btoa(str).replace(/[+=\/]/g, '')
}
}
class NativeImage {
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])
}
}
class NativeImage {
#origin
constructor(obj) {
@ -131,17 +171,17 @@ class NativeImage {
toDataURL(type) {
return this.export(type, true)
}
}
}
class EventEmitter {
class Native {
//
__events__ = Object.create(null)
[__events__] = Object.create(null)
$on(name, fn) {
if (this.__events__[name]) {
this.__events__[name].push(fn)
if (this[__events__][name]) {
this[__events__][name].push(fn)
} else {
this.__events__[name] = [fn]
this[__events__][name] = [fn]
}
}
@ -151,18 +191,20 @@ class EventEmitter {
}
$off(name, fn) {
if (this.__events__[name]) {
if (this[__events__][name]) {
if (fn) {
this.__events__[name] = this.__events__[name].filter(it => it !== fn)
this[__events__][name] = this[__events__][name].filter(
it => it !== fn
)
} else {
this.__events__[name] = []
this[__events__][name] = []
}
}
}
$emit(name, ...args) {
if (this.__events__[name]) {
for (let fn of this.__events__[name]) {
if (this[__events__][name]) {
for (let fn of this[__events__][name]) {
try {
fn.apply(this, args)
if (fn.__once__) {
@ -176,17 +218,40 @@ class EventEmitter {
}
$destroy() {
this.__events__ = Object.create(null)
this[__events__] = Object.create(null)
}
}
}
window.native = new EventEmitter()
readonly(window, 'native', new Native())
Object.assign(native, {
native.$on('opener_message', (data, uuid) => _postMessage(data, uuid))
_extend(native, {
env: '{{env}}',
app: {
name: '{{app_name}}',
version: '{{app_version}}',
quit() {
return handler('quit', {}, null)
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
@ -195,15 +260,15 @@ Object.assign(native, {
write(filepath, content = '', mode = 'w') {
return handler('fs', {
action: 'write',
append: false,
mode,
filepath,
content
})
},
append(filepath, content = '', mode = 'w') {
append(filepath, content = '', mode = 'a+') {
return handler('fs', {
action: 'write',
append: true,
mode,
filepath,
content
})
@ -228,6 +293,9 @@ Object.assign(native, {
},
copy(filepath, target) {
return handler('fs', { action: 'copy', filepath, target })
},
mkdir(filepath) {
return handler('fs', { action: 'mkdir', filepath })
}
},
image(filepath) {
@ -238,7 +306,7 @@ Object.assign(native, {
return handler('clipboard', { action: 'wait_for_text' })
},
writeText(value) {
return handler('clipboard', { action: 'set_text', value }, null)
return handler('clipboard', { action: 'set_text', value }, NO_CALLBACK)
},
readImage() {
return handler('clipboard', { action: 'wait_for_image' }).then(r =>
@ -250,10 +318,10 @@ Object.assign(native, {
if (typeof value === 'object') {
value = value.toJSON()
}
return handler('clipboard', { action: 'set_image', value }, null)
return handler('clipboard', { action: 'set_image', value }, NO_CALLBACK)
},
clear() {
return handler('clipboard', { action: 'clear' })
return handler('clipboard', { action: 'clear' }, NO_CALLBACK)
}
},
screen: {
@ -326,48 +394,87 @@ Object.assign(native, {
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' }, null)
handler('window', { action: 'toggle_visible' }, NO_CALLBACK)
},
hide() {
handler('window', { action: 'hide' }, null)
handler('window', { action: 'hide' }, NO_CALLBACK)
},
show() {
handler('window', { action: 'show' }, null)
handler('window', { action: 'show' }, NO_CALLBACK)
},
fullscreen() {
handler('window', { action: 'fullscreen' }, null)
handler('window', { action: 'fullscreen' }, NO_CALLBACK)
},
unfullscreen() {
handler('window', { action: 'unfullscreen' }, null)
handler('window', { action: 'unfullscreen' }, NO_CALLBACK)
},
maximize() {
handler('window', { action: 'maximize' }, null)
handler('window', { action: 'maximize' }, NO_CALLBACK)
},
unmaximize() {
handler('window', { action: 'unmaximize' }, null)
handler('window', { action: 'unmaximize' }, NO_CALLBACK)
},
setTitle(title = '') {
handler('window', { action: 'set_title', value: title }, null)
handler('window', { action: 'set_title', value: title }, NO_CALLBACK)
},
resize(width = 0, height = 0) {
handler('window', { action: 'resize', value: { width, height } }, null)
handler(
'window',
{ action: 'resize', value: { width, height } },
NO_CALLBACK
)
},
move(x = 0, y = 0) {
handler('window', { action: 'resize', value: { x, y } }, null)
handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK)
},
setOpacity(opacity = 1) {
handler('window', { action: 'set_opacity', value: opacity }, null)
handler(
'window',
{ action: 'set_opacity', value: opacity },
NO_CALLBACK
)
},
alwayOnTop(setting = true) {
handler('window', { action: 'set_keep_above', value: setting }, null)
handler(
'window',
{ action: 'set_keep_above', value: setting },
NO_CALLBACK
)
},
alwayOnBotttom(setting = true) {
handler('window', { action: 'set_keep_below', value: setting }, null)
handler(
'window',
{ action: 'set_keep_below', value: setting },
NO_CALLBACK
)
}
},
notify({ title, summary, icon, progress = 0, urgency = 0, callback }) {
@ -379,7 +486,7 @@ Object.assign(native, {
handler(
'notify',
{ title, summary, icon, progress, urgency, callback: eventName },
null
NO_CALLBACK
)
},
@ -387,5 +494,18 @@ Object.assign(native, {
return handler('md5', { value })
},
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)
}
},
handler
})
})
})()