diff --git a/data/ui/window.blp b/data/ui/window.blp index 0c7b324..6858aa2 100644 --- a/data/ui/window.blp +++ b/data/ui/window.blp @@ -1,5 +1,6 @@ using Gtk 4.0; using Adw 1; +using WebKit 6.0; template BavarderWindow : Adw.ApplicationWindow { @@ -139,7 +140,7 @@ template BavarderWindow : Adw.ApplicationWindow { styles ["card", "text-box"] - ScrolledWindow { + ScrolledWindow scrolled_response_window { margin-top:12; margin-bottom:0; margin-start:12; @@ -147,6 +148,8 @@ template BavarderWindow : Adw.ApplicationWindow { styles ["scrolled-window"] Gtk.Stack response_stack { + vexpand: true; + hexpand: true; Gtk.StackPage { name: "page_response"; child: TextView bot_text_view { diff --git a/src/main.py b/src/main.py index 4771f23..e8c435d 100644 --- a/src/main.py +++ b/src/main.py @@ -27,19 +27,29 @@ gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") gi.require_version("Gdk", "4.0") gi.require_version("Gst", "1.0") +gi.require_version('WebKit', '6.0') -from gi.repository import Gtk, Gio, Adw, Gdk, GLib, Gst +from gi.repository import Gtk, Gio, Adw, Gdk, GLib, Gst, WebKit from .window import BavarderWindow from .preferences import Preferences +from enum import auto, IntEnum from .constants import app_id, version -from gtts import gTTS from tempfile import NamedTemporaryFile from .provider import PROVIDERS import platform import os +import markdown +import tempfile +import re + +class Step(IntEnum): + CONVERT_HTML = auto() + LOAD_WEBVIEW = auto() + RENDER = auto() + class BavarderApplication(Adw.Application): @@ -71,6 +81,15 @@ class BavarderApplication(Adw.Application): ) self.latest_provider = self.settings.get_string("latest-provider") + self.web_view = None + self.web_view_pending_html = None + + self.loading = False + self.shown = False + self.preview_visible = False + + + def quitting(self, *args, **kwargs): """Called before closing main window.""" self.settings.set_strv("enabled-providers", list(self.enabled_providers)) @@ -113,8 +132,6 @@ class BavarderApplication(Adw.Application): self.win = BavarderWindow(application=self) self.win.present() - self.win.response_stack.set_visible_child_name("page_response") - self.win.connect("close-request", self.quitting) self.load_dropdown() @@ -261,8 +278,664 @@ Providers: {self.enabled_providers} def ask(self, prompt): return self.providers[self.provider].ask(prompt) + @staticmethod + def on_click_link(web_view, decision, _decision_type): + if web_view.get_uri().startswith(("http://", "https://", "www.")): + Glib.spawn_command_line_async(f"xdg-open {web_view.get_uri()}") + decision.ignore() + return True + + @staticmethod + def on_right_click(web_view, context_menu, _event, _hit_test): + # disable some context menu option + for item in context_menu.get_items(): + if item.get_stock_action() in [WebKit.ContextMenuAction.RELOAD, + WebKit.ContextMenuAction.GO_BACK, + WebKit.ContextMenuAction.GO_FORWARD, + WebKit.ContextMenuAction.STOP]: + context_menu.remove(item) + + + def show(self, html=None, step=Step.LOAD_WEBVIEW): + if step == Step.LOAD_WEBVIEW: + self.loading = True + if not self.web_view: + self.web_view = WebKit.WebView() + self.web_view.get_settings().set_allow_universal_access_from_file_urls(True) + #TODO: enable devtools on Devel profile + self.web_view.get_settings().set_enable_developer_extras(True) + + # Show preview once the load is finished + self.web_view.connect("load-changed", self.on_load_changed) + + # All links will be opened in default browser, but local files are opened in apps. + self.web_view.connect("decide-policy", self.on_click_link) + + self.web_view.connect("context-menu", self.on_right_click) + + self.web_view.set_hexpand(True) + self.web_view.set_vexpand(True) + + self.win.response_stack.add_child(self.web_view) + self.win.response_stack.set_visible_child(self.web_view) + + + print(html) + if self.web_view.is_loading(): + self.web_view_pending_html = html + else: + self.web_view.load_html(html, "file://localhost/") + + + elif step == Step.RENDER: + if not self.preview_visible: + self.preview_visible = True + self.show() + + def reload(self, *_widget, reshow=False): + if self.preview_visible: + if reshow: + self.hide() + self.show() + + def on_load_changed(self, _web_view, event): + if event == WebKit.LoadEvent.FINISHED: + self.loading = False + if self.web_view_pending_html: + self.show(html=self.web_view_pending_html, step=Step.LOAD_WEBVIEW) + self.web_view_pending_html = None + else: + # we only lazyload the webview once + self.show(step=Step.RENDER) + + def parse_css(path): + + adw_palette_prefixes = [ + "blue_", + "green_", + "yellow_", + "orange_", + "red_", + "purple_", + "brown_", + "light_", + "dark_" + ] + + # Regular expressions + not_define_color = re.compile(r"(^(?:(?!@define-color).)*$)") + define_color = re.compile(r"(@define-color .*[^\s])") + css = "" + variables = {} + palette = {} + + for color in adw_palette_prefixes: + palette[color] = {} + + with open(path, "r", encoding="utf-8") as sheet: + for line in sheet: + cdefine_match = re.search(define_color, line) + not_cdefine_match = re.search(not_define_color, line) + if cdefine_match != None: # If @define-color variable declarations were found + palette_part = cdefine_match.__getitem__(1) # Get the second item of the re.Match object + name, color = palette_part.split(" ", 1)[1].split(" ", 1) + if name.startswith(tuple(adw_palette_prefixes)): # Palette colors + palette[name[:-1]][name[-1:]] = color[:-1] + else: # Other color variables + variables[name] = color[:-1] + elif not_cdefine_match != None: # If CSS rules were found + css_part = not_cdefine_match.__getitem__(1) + css += f"{css_part}\n" + + sheet.close() + return variables, palette, css + def update_response(self, response): - self.win.bot_text_view.get_buffer().set_text(response) + """Update the response text view with the response.""" + response = markdown.markdown(response, extensions=["markdown.extensions.extra"]) + + TEMPLATE = """ + + + + + + {response} + + + """ + self.show(TEMPLATE.replace("{response}", response), Step.LOAD_WEBVIEW) def on_ask_action(self, widget, _): """Callback for the app.ask action.""" diff --git a/src/provider/base.py b/src/provider/base.py index 95b71dc..111cb9f 100644 --- a/src/provider/base.py +++ b/src/provider/base.py @@ -21,7 +21,6 @@ class BavarderProvider: def __init__(self, win, app, *args, **kwargs): self.win = win self.banner = win.banner - self.bot_text_view = win.bot_text_view self.app = app self.chat = None self.update_response = app.update_response diff --git a/src/window.py b/src/window.py index 9869f94..8d92669 100644 --- a/src/window.py +++ b/src/window.py @@ -27,10 +27,11 @@ class BavarderWindow(Adw.ApplicationWindow): toast_overlay = Gtk.Template.Child() prompt_text_view = Gtk.Template.Child() - bot_text_view = Gtk.Template.Child() spinner = Gtk.Template.Child() ask_button = Gtk.Template.Child() wait_button = Gtk.Template.Child() + scrolled_response_window = Gtk.Template.Child() + bot_text_view = Gtk.Template.Child() response_stack = Gtk.Template.Child() banner = Gtk.Template.Child() # listen = Gtk.Template.Child()