add tab support by having separate renderers and http clients, make link clicking open a new tab, add better default headers, fix style elements showing as text, fix crash if there are no needs but needs_render is True, fix view-source scheme not working,

This commit is contained in:
csd4ni3l
2025-07-27 16:38:56 +02:00
parent c77b067d08
commit 31b67c9dfd
5 changed files with 163 additions and 98 deletions

View File

@@ -28,7 +28,7 @@ class HTTPClient():
self.response_headers = {}
self.response_http_version = None
self.response_status = None
self.nodes = []
self.nodes = None
self.css_rules = []
self.content_response = ""
self.view_source = False
@@ -71,7 +71,7 @@ class HTTPClient():
if "Host" not in self.request_headers:
self.request_headers["Host"] = self.host
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
if os.path.exists(f"html_cache/{cache_filename}"):
threading.Thread(target=self.parse, daemon=True).start()
return
@@ -190,7 +190,14 @@ class HTTPClient():
def parse(self):
self.css_rules = []
html_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
html_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.html"
if html_cache_filename in os.listdir("html_cache"):
with open(f"html_cache/{html_cache_filename}", "r") as file:
self.content_response = file.read()
else:
with open(f"html_cache/{html_cache_filename}", "w") as file:
file.write(self.content_response)
original_scheme = self.scheme
original_host = self.host
@@ -198,15 +205,8 @@ class HTTPClient():
original_path = self.path
original_response = self.content_response
if html_cache_filename in os.listdir("html_cache"):
with open(f"html_cache/{html_cache_filename}", "r") as file:
self.nodes = HTML.from_json(ujson.load(file))
else:
self.nodes = HTML(self.content_response).parse()
with open(f"html_cache/{html_cache_filename}", "w") as file:
json_list = HTML.to_json(self.nodes)
file.write(ujson.dumps(json_list))
self.nodes = HTML(self.content_response).parse()
css_links = [
node.attributes["href"]
for node in tree_to_list(self.nodes, [])

View File

@@ -36,7 +36,8 @@ class HTML():
for c in self.raw_html:
if c == "<":
in_tag = True
if text: self.add_text(text) # start of new tag means before everything was content/text
if (not self.unfinished or not self.unfinished[-1].tag == "style") and text:
self.add_text(text) # start of new tag means before everything was content/text
text = ""
elif c == ">":
in_tag = False
@@ -173,6 +174,9 @@ def get_inline_styles(node):
for node in node.children:
if isinstance(node, Element) and node.tag == "style":
if not node.children:
continue
if isinstance(node.children[0], Text):
all_rules.extend(CSSParser(node.children[0].text).parse()) # node's first children will just be a text element that contains the css

View File

@@ -3,8 +3,8 @@ import arcade, pyglet, platform
from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern, INHERITED_PROPERTIES
from utils.utils import get_color_from_name, hex_to_rgb
from http_client.connection import HTTPClient, resolve_url
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols, tree_to_list
from http_client.connection import HTTPClient
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority
from pyglet.font.base import Font as BaseFont
@@ -245,11 +245,11 @@ def paint_tree(layout_object, display_list):
paint_tree(child, display_list)
class Renderer():
def __init__(self, http_client: HTTPClient, view_class):
def __init__(self, http_client: HTTPClient, window):
self.content = ''
self.request_scheme = 'http'
self.view_class = view_class
self.window: arcade.Window = view_class.window
self.window: arcade.Window = window
self.current_window_size = self.window.size
self.http_client = http_client
self.scroll_y = 0
@@ -261,10 +261,6 @@ class Renderer():
self.widgets: list[pyglet.text.Label] = []
self.text_to_create = []
self.window.on_mouse_scroll = self.on_mouse_scroll
self.window.on_mouse_press = self.on_mouse_press
self.window.on_resize = self.on_resize
self.batch = pyglet.graphics.Batch()
def hide_out_of_bounds_labels(self):
@@ -279,8 +275,8 @@ class Renderer():
widget.visible = True
def on_resize(self, width, height):
if self.http_client.css_rules:
self.http_client.needs_render = True
self.current_window_size = self.window.size
self.http_client.needs_render = True
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.allow_scroll:
@@ -289,40 +285,11 @@ class Renderer():
old_y = self.scroll_y
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) + 5)) # flip scroll direction
for widget in self.widgets:
widget.y += (self.scroll_y - old_y)
self.hide_out_of_bounds_labels()
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
if not self.document:
return
y -= self.scroll_y
objs = [
obj for obj in tree_to_list(self.document, [])
if obj.x <= x < obj.x + obj.width
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
]
if not objs:
return
elt = objs[-1].node
while elt:
if isinstance(elt, Text):
pass
elif elt.tag == "a" and "href" in elt.attributes:
url = resolve_url(self.http_client.scheme, self.http_client.host, self.http_client.port, self.http_client.path, elt.attributes["href"])
self.http_client.get_request(url, self.http_client.request_headers)
self.view_class.search_bar.text = url
elt = elt.parent
def add_text(self, x, y, text, font, color, multiline=False):
self.widgets.append(
pyglet.text.Label(
@@ -331,6 +298,7 @@ class Renderer():
italic=font.italic,
weight=font.weight,
font_size=font.size,
width=self.window.width * 0.5 if multiline else None,
multiline=multiline,
color=color,
x=x,
@@ -369,19 +337,21 @@ class Renderer():
self.smallest_y = 0
if self.http_client.view_source or self.http_client.scheme == "file":
self.add_text(x=HSTEP, y=0, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), multiline=True)
self.add_text(x=HSTEP, y=self.window.height * 0.05, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), color=arcade.color.BLACK, multiline=True)
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
if self.http_client.nodes:
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
self.document = DocumentLayout(self.http_client.nodes)
self.document.layout()
self.cmds = []
paint_tree(self.document, self.cmds)
for cmd in self.cmds:
if isinstance(cmd, DrawText):
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
elif isinstance(cmd, DrawRect):
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
self.document = DocumentLayout(self.http_client.nodes)
self.document.layout()
self.cmds = []
paint_tree(self.document, self.cmds)
for cmd in self.cmds:
if isinstance(cmd, DrawText):
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
elif isinstance(cmd, DrawRect):
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
self.hide_out_of_bounds_labels()
self.hide_out_of_bounds_labels()

View File

@@ -1,11 +1,44 @@
import arcade, arcade.gui, asyncio, pypresence, time, copy, json, asyncio
from utils.constants import discord_presence_id
from utils.constants import discord_presence_id, DEFAULT_HEADERS
from utils.utils import FakePyPresence
from http_client.connection import HTTPClient
from http_client.connection import HTTPClient, resolve_url
from http_client.html_parser import tree_to_list, Text
from http_client.renderer import Renderer
class Tab():
def __init__(self, url, window, tab_button, pypresence_client):
self.pypresence_client = pypresence_client
self.tab_button = tab_button
self.window = window
self.http_client = HTTPClient()
self.renderer = Renderer(self.http_client, window)
self.request(url)
def request(self, url):
if url.startswith("http://") or url.startswith("https://") or url.startswith("view-source:"):
self.http_client.get_request(url, DEFAULT_HEADERS)
elif url.startswith("file://"):
self.http_client.file_request(url)
elif url.startswith("data:text/html,"):
self.http_client.content_response = url.split("data:text/html,")[1]
self.http_client.scheme = "http"
elif url == "about:blank":
self.http_client.content_response = ""
self.http_client.scheme = "http"
elif url == "about:config" or url == "about:settings":
self.settings()
else:
self.http_client.get_request(f"https://{url}", DEFAULT_HEADERS)
self.tab_button.text = url
def settings(self):
from menus.settings import Settings
self.window.show_view(Settings(self.pypresence_client))
class Main(arcade.gui.UIView):
def __init__(self, pypresence_client=None):
super().__init__()
@@ -47,17 +80,65 @@ class Main(arcade.gui.UIView):
self.pypresence_client.update(state='Browsing', details='In the browser', start=self.pypresence_client.start_time)
self.http_client = HTTPClient()
def on_resize(self, width, height):
self.ui.clear()
self.on_show_view()
self.tabs: list[Tab] = []
self.tab_buttons = []
self.active_tab = None
def on_show_view(self):
super().on_show_view()
self.search_bar = self.add_widget(arcade.gui.UIInputText(x=self.window.width / 4, y=self.window.height * 0.95, width=self.window.width / 2, height=self.window.height * 0.035, font_name="Roboto", font_size=14, text_color=arcade.color.BLACK, caret_color=arcade.color.BLACK, border_color=arcade.color.BLACK))
self.renderer = Renderer(self.http_client, self)
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.navigation_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="top")
self.tab_box = self.navigation_box.add(arcade.gui.UIBoxLayout(space_between=5, vertical=False))
self.new_tab_button = self.tab_box.add(arcade.gui.UIFlatButton(text="+", width=self.window.width / 25, height=30, style=arcade.gui.UIFlatButton.DEFAULT_STYLE))
self.new_tab_button.on_click = lambda event: self.new_tab()
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text="about:blank", width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
self.search_bar = self.navigation_box.add(arcade.gui.UIInputText(width=self.window.width * 0.5, height=30, font_name="Roboto", font_size=14, text_color=arcade.color.BLACK, caret_color=arcade.color.BLACK, border_color=arcade.color.BLACK))
default_tab = Tab("about:blank", self.window, self.tab_buttons[0], self.pypresence_client)
self.tabs.append(default_tab)
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[0]: self.switch_to_tab(tab)
self.switch_to_tab(default_tab)
def search(self):
url = self.search_bar.text
self.active_tab.request(url)
def switch_to_tab(self, tab):
if self.active_tab:
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.DEFAULT_STYLE
self.active_tab = tab
self.active_tab.tab_button.style = arcade.gui.UIFlatButton.STYLE_BLUE
if self.active_tab.renderer.current_window_size != self.window.size:
self.active_tab.renderer.on_resize(self.window.width, self.window.height)
self.window.on_mouse_scroll = self.active_tab.renderer.on_mouse_scroll
http_client = self.active_tab.http_client
if http_client.scheme and http_client.host and http_client.path:
port_str = f':{http_client.port}' if not http_client.port in [80, 443, 0] else ''
self.search_bar.text = f"{http_client.scheme}://{http_client.host}{port_str}{http_client.path}"
def new_tab(self, url="about:blank"):
self.tab_buttons.append(self.tab_box.add(arcade.gui.UIFlatButton(text=url, width=self.window.width / 7, height=30, style=arcade.gui.UIFlatButton.STYLE_BLUE)))
self.tabs.append(Tab(url, self.window, self.tab_buttons[-1], self.pypresence_client))
self.tab_buttons[-1].on_click = lambda event, tab=self.tabs[-1]: self.switch_to_tab(tab)
self.switch_to_tab(self.tabs[-1])
def on_resize(self, width, height):
for tab_button in self.tab_buttons:
tab_button.rect = tab_button.rect.resize(width / 7, 30)
self.active_tab.renderer.on_resize(width, height)
def on_key_press(self, symbol, modifiers):
self.search_bar.text = self.search_bar.text.encode("ascii", "ignore").decode().strip("\n")
@@ -65,34 +146,36 @@ class Main(arcade.gui.UIView):
self.search()
def on_update(self, delta_time):
self.renderer.update()
self.active_tab.renderer.update()
def search(self):
url = self.search_bar.text
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
if not self.active_tab.renderer.document:
return
y -= self.active_tab.renderer.scroll_y
objs = [
obj for obj in tree_to_list(self.active_tab.renderer.document, [])
if obj.x <= x < obj.x + obj.width
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
]
if url.startswith("http://") or url.startswith("https://") or url.startswith("view-source:"):
self.http_client.get_request(url, {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"})
elif url.startswith("file://"):
self.http_client.file_request(url)
elif url.startswith("data:text/html,"):
self.http_client.content_response = url.split("data:text/html,")[1]
self.http_client.scheme = "http"
elif url == "about:blank":
self.http_client.content_response = ""
self.http_client.scheme = "http"
elif url == "about:config" or url == "about:settings":
self.settings()
else:
self.http_client.get_request(f"https://{url}", {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0"})
self.search_bar.text = f"https://{url}"
if not objs:
return
elt = objs[-1].node
self.search_bar.text = self.search_bar.text.encode("ascii", "ignore").decode().strip("\n")
while elt:
if isinstance(elt, Text):
pass
elif elt.tag == "a" and "href" in elt.attributes:
url = resolve_url(self.active_tab.http_client.scheme, self.active_tab.http_client.host, self.active_tab.http_client.port, self.active_tab.http_client.path, elt.attributes["href"])
self.new_tab(url)
return
def settings(self):
from menus.settings import Settings
self.window.show_view(Settings(self.pypresence_client))
elt = elt.parent
def on_draw(self):
super().on_draw()
self.renderer.batch.draw()
self.active_tab.renderer.batch.draw()

View File

@@ -62,6 +62,14 @@ INHERITED_PROPERTIES = {
"height": "auto"
}
DEFAULT_HEADERS = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0",
"Accept": "text/html",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none"
}
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),