Compare commits

...

10 Commits

9 changed files with 602 additions and 473 deletions

View File

@ -22,6 +22,7 @@ sed -i "s/{{size}}/${_size}/" DEBIAN/control
sed -i "s/{{version}}/${version}/" DEBIAN/control sed -i "s/{{version}}/${version}/" DEBIAN/control
cd .. cd ..
sudo chown -R root:root unpack/
dpkg-deb -b unpack/ "python3-webengine-gtk3-${version}.deb" dpkg-deb -b unpack/ "python3-webengine-gtk3-${version}.deb"
sudo rm -rf unpack sudo rm -rf unpack

2
debian/control vendored
View File

@ -4,7 +4,7 @@ Section: develop
Priority: optional Priority: optional
Maintainer: Yutent <yutent.io@gmail.com> Maintainer: Yutent <yutent.io@gmail.com>
Architecture: all 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 Recommends: gir1.2-ayatanaappindicator3-0.1, gir1.2-notify-0.7, gir1.2-keybinder-3.0
Installed-Size: {{size}} Installed-Size: {{size}}
Homepage: https://git.wkit.fun/appcat/python3-webengine-gtk3 Homepage: https://git.wkit.fun/appcat/python3-webengine-gtk3

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

@ -15,11 +15,16 @@ class Inject:
self.manager = webview.get_user_content_manager() self.manager = webview.get_user_content_manager()
script_data = open(self.abspath('./inject.js'), 'r').read() code = open(self.abspath('./inject.js'), 'r').read()
frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES
time = WebKit2.UserScriptInjectionTime.END time = WebKit2.UserScriptInjectionTime.START
script_data = script_data.replace("'{{env}}'", json.dumps(env)).replace("{{uuid}}", webview.uuid)
script = WebKit2.UserScript(script_data, frame, time, None, None) 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) self.manager.add_script(script)

View File

@ -22,7 +22,7 @@ class Settings(WebKit2.Settings):
self.set_javascript_can_open_windows_automatically(True) self.set_javascript_can_open_windows_automatically(True)
self.set_user_agent_with_application_details('WebEngine', version) self.set_useragent_with_app('WebEngine', version)
# indexedDB 和 localStorage 和 离线缓存 # indexedDB 和 localStorage 和 离线缓存
@ -43,6 +43,9 @@ class Settings(WebKit2.Settings):
self.set_media_playback_allows_inline(True) 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): def set_useragent(self, str):
self.set_user_agent(str) self.set_user_agent(str)
@ -75,6 +78,9 @@ def create_setting(options = None):
setting.enable_devtools() setting.enable_devtools()
setting.mock_devices() 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'): if options.get('useragent'):
setting.set_useragent(options.get('useragent')) setting.set_useragent(options.get('useragent'))
@ -86,6 +92,9 @@ def create_setting(options = None):
def wrapper(app, extra = None): 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) app.set_settings(setting)
return wrapper return wrapper

View File

@ -3,9 +3,12 @@
# @author yutent<yutent.io@gmail.com> # @author yutent<yutent.io@gmail.com>
# @date 2023/07/28 14:39:33 # @date 2023/07/28 14:39:33
import gi import gi, threading
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf, GObject
def noop():
pass
def get_monitor_info(monitor): def get_monitor_info(monitor):
return { return {
@ -64,3 +67,33 @@ def dict_to_pixbuf(data):
image = None image = None
return image 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

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

View File

@ -3,7 +3,8 @@
# @date 2023/08/08 14:07:26 # @date 2023/08/08 14:07:26
import gi, os, json, shutil, hashlib, random import gi, os, sys, json
import webbrowser, shutil, hashlib, random
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
gi.require_version("WebKit2", "4.1") gi.require_version("WebKit2", "4.1")
@ -31,27 +32,13 @@ try:
except: except:
Keybinder = None Keybinder = None
from ._env import env
from ._version import version
from ._settings import create_setting from ._settings import create_setting
from ._protocal import create_protocal from ._protocal import create_protocal
from ._notify import create_notify from ._notify import create_notify
from ._inject import Inject 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
env = {
"HOME_DIR": os.getenv('HOME'),
"CONFIG_DIR": os.path.join(os.getenv('HOME'), '.config'),
"CACHE_DIR": os.path.join(os.getenv('HOME'), '.cache')
}
def noop():
pass
@ -60,7 +47,8 @@ class WebEngine(WebKit2.WebView):
__gsignals__ = { __gsignals__ = {
'quit': (GObject.SignalFlags.RUN_FIRST, None, ()) 'quit': (GObject.SignalFlags.RUN_FIRST, None, ())
} }
app_name = 'WebEngine'
app_version = version
uuid = None uuid = None
root = None root = None
window = None window = None
@ -84,7 +72,7 @@ class WebEngine(WebKit2.WebView):
opener.children.add(self) opener.children.add(self)
if win.get_title() is None: if win.get_title() is None:
win.set_title('WebEngine') win.set_title(self.app_name)
if win.get_icon() is None and win.get_icon_name() is None: if win.get_icon() is None and win.get_icon_name() is None:
win.set_icon_name('web-browser') win.set_icon_name('web-browser')
@ -108,7 +96,11 @@ class WebEngine(WebKit2.WebView):
# 允许前端 widnow.close() 关闭窗口 # 允许前端 widnow.close() 关闭窗口
self.connect('close', self.close_window) self.connect('close', self.close_window)
win.connect("destroy", self.remove_from_opener) # 通过外部关闭窗口时从父级中移除
# 通过外部关闭窗口时从父级中移除
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 remove_from_opener(self, win = None): def remove_from_opener(self, win = None):
@ -163,14 +155,14 @@ class WebEngine(WebKit2.WebView):
raise ValueError('url must starts with "/" or "app://"') raise ValueError('url must starts with "/" or "app://"')
@idle
def call_js(self, method, data = None, err = None): def call_js(self, method, data = None, err = None):
if err is not None: if err is not None:
err = str(err) err = str(err)
scripts = 'native.$emit("' + method + '", ' + json.dumps(err) + ', ' + json.dumps(data) + ')' scripts = 'native.$emit("' + method + '", ' + json.dumps(err) + ', ' + json.dumps(data) + ')'
self.evaluate_javascript(scripts, -1) self.evaluate_javascript(scripts, -1)
@run_async
def called_by_js(self, webview, message): def called_by_js(self, webview, message):
data = json.loads(message.get_js_value().to_json(0)) data = json.loads(message.get_js_value().to_json(0))
@ -183,21 +175,16 @@ class WebEngine(WebKit2.WebView):
match event: match event:
case 'init':
output = env case 'app':
_error, output = self._app_handler(params)
case 'fs': case 'fs':
_error, output = self._fs_setting(params) _error, output = self._fs_handler(params)
case 'clipboard': case 'clipboard':
_error, output = self._clipboard_setting(params) _error, output = self._clipboard_handler(params)
# 退出app
case 'quit':
self.close_window()
self.emit('quit')
# 读取图片, 返回图片像素数据 # 读取图片, 返回图片像素数据
case 'image': case 'image':
@ -217,7 +204,7 @@ class WebEngine(WebKit2.WebView):
case 'keybinder': case 'keybinder':
_error, output = self._keybinder_setting(params) _error, output = self._keybinder_handler(params)
case 'tray': case 'tray':
@ -257,7 +244,7 @@ class WebEngine(WebKit2.WebView):
return return
case 'window': case 'window':
_error, output = self._window_setting(params) _error, output = self._window_handler(params)
case 'notify': case 'notify':
@ -274,25 +261,61 @@ class WebEngine(WebKit2.WebView):
case 'proxy': case 'proxy':
_error, output = self._proxy_setting(params) _error, output = self._proxy_handler(params)
case 'md5': case 'md5':
output = hashlib.md5(str(params.get('value'))).hexdigest() output = hashlib.md5(str(params.get('value'))).hexdigest()
case _: case _:
if self.custom_bridge is None: if self.custom_bridge is not None:
pass try:
else: _error, output = self.custom_bridge(event, params) or (None, None)
_error, output = self.custom_bridge(event, params) except Exception as err:
print(err)
_error = err
# 有回调则返回结果 # 有回调则返回结果
if callback: if callback:
self.call_js(callback, output, _error) 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')
def _fs_setting(self, params = {}): 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 _error = None
output = None output = None
filepath = params.get('filepath') filepath = params.get('filepath')
@ -362,11 +385,14 @@ class WebEngine(WebKit2.WebView):
case 'isdir': case 'isdir':
output = os.path.isdir(filepath) output = os.path.isdir(filepath)
case 'mkdir':
output = os.makedirs(filepath)
return (_error, output) return (_error, output)
def _clipboard_setting(self, params = {}): def _clipboard_handler(self, params = {}):
_error = None _error = None
output = None output = None
@ -403,7 +429,7 @@ class WebEngine(WebKit2.WebView):
return (_error, output) return (_error, output)
def _keybinder_setting(self, params = {}): def _keybinder_handler(self, params = {}):
_error = None _error = None
output = None output = None
keymap = params.get('value') keymap = params.get('value')
@ -434,7 +460,7 @@ class WebEngine(WebKit2.WebView):
def _window_setting(self, params = {}): def _window_handler(self, params = {}):
_error = None _error = None
output = None output = None
@ -496,7 +522,7 @@ class WebEngine(WebKit2.WebView):
def _proxy_setting(self, params = {}): def _proxy_handler(self, params = {}):
dm = self.get_website_data_manager() dm = self.get_website_data_manager()
output = True output = True
_error = None _error = None

View File

@ -3,7 +3,7 @@
* @author yutent<yutent.io@gmail.com> * @author yutent<yutent.io@gmail.com>
* @date 2023/07/21 17:38:11 * @date 2023/07/21 17:38:11
*/ */
!(function () {
const MIME_TYPES = { const MIME_TYPES = {
html: 'text/html', html: 'text/html',
json: 'application/json', json: 'application/json',
@ -48,6 +48,8 @@ const KEYS_MAP = {
const NO_CALLBACK = false const NO_CALLBACK = false
const CALL_ONCE = true const CALL_ONCE = true
const __events__ = Symbol('events')
function defer() { function defer() {
let obj = {} let obj = {}
obj.promise = new Promise((resolve, reject) => { obj.promise = new Promise((resolve, reject) => {
@ -71,7 +73,7 @@ function handler(event, data = {}, need = CALL_ONCE) {
callback = rand() callback = rand()
native.$once(callback, (err, res) => { native.$once(callback, (err, res) => {
if (err) { if (err) {
_.reject(err) _.reject(new Error(err))
} else { } else {
_.resolve(res) _.resolve(res)
} }
@ -103,6 +105,21 @@ function _postMessage(data = {}, uuid = null) {
window.dispatchEvent(ev) 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 { class NativeImage {
#origin #origin
@ -156,15 +173,15 @@ class NativeImage {
} }
} }
class EventEmitter { class Native {
// //
__events__ = Object.create(null) [__events__] = Object.create(null)
$on(name, fn) { $on(name, fn) {
if (this.__events__[name]) { if (this[__events__][name]) {
this.__events__[name].push(fn) this[__events__][name].push(fn)
} else { } else {
this.__events__[name] = [fn] this[__events__][name] = [fn]
} }
} }
@ -174,18 +191,20 @@ class EventEmitter {
} }
$off(name, fn) { $off(name, fn) {
if (this.__events__[name]) { if (this[__events__][name]) {
if (fn) { if (fn) {
this.__events__[name] = this.__events__[name].filter(it => it !== fn) this[__events__][name] = this[__events__][name].filter(
it => it !== fn
)
} else { } else {
this.__events__[name] = [] this[__events__][name] = []
} }
} }
} }
$emit(name, ...args) { $emit(name, ...args) {
if (this.__events__[name]) { if (this[__events__][name]) {
for (let fn of this.__events__[name]) { for (let fn of this[__events__][name]) {
try { try {
fn.apply(this, args) fn.apply(this, args)
if (fn.__once__) { if (fn.__once__) {
@ -199,19 +218,34 @@ class EventEmitter {
} }
$destroy() { $destroy() {
this.__events__ = Object.create(null) this[__events__] = Object.create(null)
} }
} }
window.native = new EventEmitter() readonly(window, 'native', new Native())
native.$on('opener_message', (data, uuid) => _postMessage(data, uuid)) native.$on('opener_message', (data, uuid) => _postMessage(data, uuid))
Object.assign(native, { _extend(native, {
env: '{{env}}', env: '{{env}}',
app: {
name: '{{app_name}}',
version: '{{app_version}}',
quit() { quit() {
return handler('quit', {}, NO_CALLBACK) 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: { fs: {
access(filepath, mode = 'r') { access(filepath, mode = 'r') {
@ -259,6 +293,9 @@ Object.assign(native, {
}, },
copy(filepath, target) { copy(filepath, target) {
return handler('fs', { action: 'copy', filepath, target }) return handler('fs', { action: 'copy', filepath, target })
},
mkdir(filepath) {
return handler('fs', { action: 'mkdir', filepath })
} }
}, },
image(filepath) { image(filepath) {
@ -419,7 +456,11 @@ Object.assign(native, {
handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK) handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK)
}, },
setOpacity(opacity = 1) { setOpacity(opacity = 1) {
handler('window', { action: 'set_opacity', value: opacity }, NO_CALLBACK) handler(
'window',
{ action: 'set_opacity', value: opacity },
NO_CALLBACK
)
}, },
alwayOnTop(setting = true) { alwayOnTop(setting = true) {
handler( handler(
@ -467,3 +508,4 @@ Object.assign(native, {
handler handler
}) })
})()