#!/usr/bin/python3 import gi, json, os, shutil, hashlib gi.require_version("Gtk", "3.0") gi.require_version("WebKit2", "4.1") gi.require_version("Keybinder", "3.0") from gi.repository import Gtk, Gdk, WebKit2, GLib, Gio, Keybinder from gi.repository.GdkPixbuf import Pixbuf from notes.utils import * from notes.mimetypes import get_mimetype from notes.notify import Notification # 优先尝试使用指示器, 没有再使用 Gtk.StatusIcon try: gi.require_version('AyatanaAppIndicator3', '0.1') # 需要安装这个包 gir1.2-ayatanaappindicator3-0.1 from gi.repository import AyatanaAppIndicator3 as AppIndicator3 except (ValueError, ImportError): AppIndicator3 = None # 初始化 Keybinder Keybinder.init() class WebKitWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title="WebKit Example") self.notify = Notification(self) self.display = Gdk.Display.get_default() self.set_default_size(800, 600) settings = WebKit2.Settings() settings.set_enable_page_cache(True) settings.set_enable_offline_web_application_cache(True) settings.set_enable_developer_extras(True) # settings.set_disable_web_security(True) settings.set_enable_html5_database(True) settings.set_enable_html5_local_storage(True) settings.set_javascript_can_access_clipboard(True) settings.set_javascript_can_open_windows_automatically(True) settings.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) weapp/1.0.0 Version/16.4 Safari/605.1.15") manager = WebKit2.UserContentManager() script = open(self.file_path('./inject.js'), 'r').read() frame = WebKit2.UserContentInjectedFrames.ALL_FRAMES time = WebKit2.UserScriptInjectionTime.END script = WebKit2.UserScript(script, frame, time, None, None) manager.add_script(script) manager.connect('script-message-received::app', self.on_script_message) manager.register_script_message_handler('app') self.webview = WebKit2.WebView.new_with_user_content_manager(manager) self.webview.set_settings(settings) context = self.webview.get_context() context.register_uri_scheme('app', self.resource_request_callback) # 允许网页通知权限 context.initialize_notification_permissions([WebKit2.SecurityOrigin.new_for_uri('app:///index.html')], []) im = self.webview.get_input_method_context() im.set_enable_preedit(True) # self.webview.load_uri("app:///index.html") self.webview.load_uri("https://benchmark.wkit.fun") # 解决输入法候选框跟随问题 im.connect('preedit-started', self.on_preedit_changed) im.connect('preedit-changed', self.on_preedit_changed) im.connect('preedit-finished', self.on_preedit_changed) self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.add(self.webview) def on_preedit_changed(self, im): p = self.display.get_pointer() # 光标位置 x, y = self.get_position() # 窗口位置 im.notify_focus_in() im.notify_cursor_area(p.x - x, p.y - y, 0, 0) # 修正输入法跟随 def resource_request_callback(self, req): schema = req.get_scheme() pathname = req.get_path() ext = pathname.split('.')[-1] if schema == 'app': data = open(self.file_path('./webview' + pathname)).read() data = Gio.MemoryInputStream.new_from_data(data.encode()) req.finish(data, -1, get_mimetype(ext)) return True else: return False def create_tray(self): if AppIndicator3 : indicator = AppIndicator3.Indicator.new( "youtube", "youtube", AppIndicator3.IndicatorCategory.APPLICATION_STATUS ) # indicator.set_title('alacritty 6666') # indicator.set_label('alacritty 8888', '') # indicator.set_icon_full('alacritty','alacritty') # indicator.set_attention_icon_full('alacritty', 'alacritty') # indicator.set_ordering_index(99) indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) # indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION) menu = Gtk.Menu() menu_items = Gtk.MenuItem.new_with_label("Toggle Floater") menu.append(menu_items) menu_items.connect("activate", self.toggle_visible) menu.show_all() indicator.set_menu(menu) indicator.set_secondary_activate_target(menu_items) else: # windows 和 macos 必须传二进制图标, linux可传图标名称(自会去主题中找) # indicator = Gtk.StatusIcon.new_from_pixbuf(get_logo(32)) # linux # indicator = Gtk.StatusIcon.new_from_icon_name('youtube') # return indicator indicator = Gtk.StatusIcon.new_from_icon_name('youtube') indicator.connect('activate', self.toggle_visible) return indicator def toggle_visible(self, icon): if self.is_visible(): self.hide() else: self.present() def call_js(self, method, data = None): scripts = 'native.$emit("' + method + '",' + json.dumps(data) + ')' self.webview.evaluate_javascript(scripts, -1) def file_path(self, filepath): root = os.path.dirname(os.path.realpath(__file__)) return os.path.join(root, filepath) def on_script_message(self, webview, message): data = json.loads(message.get_js_value().to_json(0)) event = data.get('event') callback = data.get('callback') params = data.get('data') output = None match event: case 'fs': filepath = params.get('filepath') if params['action'] == 'read': with open(filepath, params.get('mode')) as file: output = file.read() if params.get('mode').find('b') > -1: output = list(output) if params['action'] == 'write': # 调整以支持二进制数据写入 with open(filepath, params.get('mode')) as file: buff = params['content'] if params.get('mode').find('b') > -1: buff = bytes(buff) output = file.write(buff) if params['action'] == 'exists': output = os.path.exists(filepath) if params['action'] == 'list': with os.scandir(filepath) as entries: output = [{ "name": it.name, "path": os.path.join(filepath, it.name), "is_dir": it.is_dir(), "size": it.stat().st_size, "atime": int(it.stat().st_atime), "mtime": int(it.stat().st_mtime), } for it in entries] if params['action'] == 'remove': if os.path.isfile(filepath): output = os.remove(filepath) elif os.path.isdir(filepath): output = os.removedirs(filename) if params['action'] == 'rename': if os.path.exists(filepath): output = shutil.move(filepath, params['target']) if params['action'] == 'copy': if os.path.exists(filepath): output = shutil.copy2(filepath, params['target']) if params['action'] == 'isfile': output = os.path.isfile(filepath) if params['action'] == 'isdir': output = os.path.isdir(filepath) case 'clipboard': # 读文本 if params['action'] == 'wait_for_text': output = self.clipboard.wait_for_text() # 写文本 elif params['action'] == 'set_text': self.clipboard.set_text(params['value'], -1) # 写图片 elif params['action'] == 'set_image': image = params['value'] # 前端传进来的值, 如果是路径的话, 直接读取 if type(image) == str: image = 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': all_quit(self) # 读取图片, 返回图片像素数据 case 'image': filename = params['value'] pixbuf = Pixbuf.new_from_file(filename) output = pixbuf_to_dict(pixbuf, filename) case 'monitor': if params['action'] == 'get-all': monitor_num = self.display.get_n_monitors() monitors = [self.display.get_monitor(i) for i in range(monitor_num)] output = [get_monitor_info(m) for m in monitors] elif params['action'] == 'get-primary': monitor = self.display.get_primary_monitor() output = get_monitor_info(monitor) case 'keybinder': keymap = params.get('value') shortcut_callback = params.get('shortcut_callback') or '' if params['action'] == 'register': # 绑定之前, 先解绑, 避免被重复绑定 Keybinder.unbind(keymap) output = Keybinder.bind( keymap, lambda km : self.call_js(shortcut_callback) ) elif params['action'] == 'unregister': Keybinder.unbind(keymap) output = True elif params['action'] == 'supported': output = Keybinder.supported() case 'tray': if params['action'] == 'create': pass elif params['action'] == 'remove': pass case 'window': if params['action'] == 'fullscreen': self.fullscreen() elif params['action'] == 'unfullscreen': self.unfullscreen() elif params['action'] == 'maximize': self.maximize() elif params['action'] == 'unmaximize': self.unmaximize() elif params['action'] == 'set_title': self.set_title(params['value'] or '') elif params['action'] == 'resize': self.resize(params['value'].get('width'), params['value'].get('height')) elif params['action'] == 'set_opacity': self.set_opacity(params['value']) elif params['action'] == 'set_keep_above': self.set_keep_above(params['value']) elif params['action'] == 'set_keep_below': self.set_keep_below(params['value']) elif params['action'] == 'move': self.move(params['value'].get('x'), params['value'].get('y')) elif params['action'] == 'toggle_visible': if self.is_visible(): self.hide() else: self.present() elif params['action'] == 'hide': self.hide() elif params['action'] == 'show': self.present() elif params['action'] == 'is_visible': output = self.is_visible() case 'notify': title = params.get('title') summary = params.get('summary') icon = params.get('icon') progress = params.get('progress') urgency = params.get('urgency') self.notify.create(title, summary, icon, progress, urgency, params.get('callback')) case 'md5': output = hashlib.md5(str(params.get('value'))).hexdigest() # 有回调则返回结果 if callback: self.call_js(callback, output) def all_quit(win): print('朕要休息了~~~') Gtk.main_quit() win = WebKitWindow() win.connect("destroy", all_quit) win.show_all() tray = win.create_tray() Gtk.main()