Update:
1. Add basic Micron parser and link support 2. Improve styling/layout 3. Add hot reloading for RNS
This commit is contained in:
@@ -15,6 +15,7 @@ from ren_browser.ui.ui import build_ui
|
|||||||
|
|
||||||
RENDERER = "plaintext"
|
RENDERER = "plaintext"
|
||||||
RNS_CONFIG_DIR = None
|
RNS_CONFIG_DIR = None
|
||||||
|
RNS_INSTANCE = None
|
||||||
|
|
||||||
|
|
||||||
async def main(page: Page):
|
async def main(page: Page):
|
||||||
@@ -79,7 +80,8 @@ async def main(page: Page):
|
|||||||
import ren_browser.logs
|
import ren_browser.logs
|
||||||
|
|
||||||
ren_browser.logs.setup_rns_logging()
|
ren_browser.logs.setup_rns_logging()
|
||||||
RNS.Reticulum(str(config_dir))
|
global RNS_INSTANCE
|
||||||
|
RNS_INSTANCE = RNS.Reticulum(str(config_dir))
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
pass
|
pass
|
||||||
page.controls.clear()
|
page.controls.clear()
|
||||||
@@ -89,6 +91,75 @@ async def main(page: Page):
|
|||||||
page.run_thread(init_ret)
|
page.run_thread(init_ret)
|
||||||
|
|
||||||
|
|
||||||
|
def reload_reticulum(page: Page, on_complete=None):
|
||||||
|
"""Hot reload Reticulum with updated configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: Flet page instance
|
||||||
|
on_complete: Optional callback to run when reload is complete
|
||||||
|
|
||||||
|
"""
|
||||||
|
def reload_thread():
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
global RNS_INSTANCE
|
||||||
|
|
||||||
|
if RNS_INSTANCE:
|
||||||
|
try:
|
||||||
|
RNS_INSTANCE.exit_handler()
|
||||||
|
print("RNS exit handler completed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning during RNS shutdown: {e}")
|
||||||
|
|
||||||
|
RNS.Reticulum._Reticulum__instance = None
|
||||||
|
|
||||||
|
RNS.Transport.destinations = []
|
||||||
|
|
||||||
|
RNS_INSTANCE = None
|
||||||
|
print("RNS instance cleared")
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Initialize storage system
|
||||||
|
storage = initialize_storage(page)
|
||||||
|
|
||||||
|
# Get Reticulum config directory from storage manager
|
||||||
|
config_dir = storage.get_reticulum_config_path()
|
||||||
|
|
||||||
|
# Ensure any saved config is written to filesystem before RNS init
|
||||||
|
try:
|
||||||
|
saved_config = storage.load_config()
|
||||||
|
if saved_config and saved_config.strip():
|
||||||
|
config_file_path = config_dir / "config"
|
||||||
|
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_file_path.write_text(saved_config, encoding="utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to write config file: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Re-initialize Reticulum
|
||||||
|
import ren_browser.logs
|
||||||
|
ren_browser.logs.setup_rns_logging()
|
||||||
|
RNS_INSTANCE = RNS.Reticulum(str(config_dir))
|
||||||
|
|
||||||
|
# Success
|
||||||
|
if on_complete:
|
||||||
|
on_complete(True, None)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reinitializing Reticulum: {e}")
|
||||||
|
if on_complete:
|
||||||
|
on_complete(False, str(e))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during reload: {e}")
|
||||||
|
if on_complete:
|
||||||
|
on_complete(False, str(e))
|
||||||
|
|
||||||
|
page.run_thread(reload_thread)
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
"""Run Ren Browser with command line argument parsing."""
|
"""Run Ren Browser with command line argument parsing."""
|
||||||
global RENDERER, RNS_CONFIG_DIR
|
global RENDERER, RNS_CONFIG_DIR
|
||||||
@@ -101,10 +172,10 @@ def run():
|
|||||||
help="Select renderer (plaintext or micron)",
|
help="Select renderer (plaintext or micron)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-w", "--web", action="store_true", help="Launch in web browser mode"
|
"-w", "--web", action="store_true", help="Launch in web browser mode",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p", "--port", type=int, default=None, help="Port for web server"
|
"-p", "--port", type=int, default=None, help="Port for web server",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class PageFetcher:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}"
|
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}",
|
||||||
)
|
)
|
||||||
dest_bytes = bytes.fromhex(req.destination_hash)
|
dest_bytes = bytes.fromhex(req.destination_hash)
|
||||||
if not RNS.Transport.has_path(dest_bytes):
|
if not RNS.Transport.has_path(dest_bytes):
|
||||||
@@ -87,11 +87,11 @@ class PageFetcher:
|
|||||||
req.field_data,
|
req.field_data,
|
||||||
response_callback=on_response,
|
response_callback=on_response,
|
||||||
failed_callback=on_failed,
|
failed_callback=on_failed,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
ev.wait(timeout=15)
|
ev.wait(timeout=15)
|
||||||
data_str = result["data"] or "No content received"
|
data_str = result["data"] or "No content received"
|
||||||
RNS.log(
|
RNS.log(
|
||||||
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}"
|
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}",
|
||||||
)
|
)
|
||||||
return data_str
|
return data_str
|
||||||
|
|||||||
@@ -1,27 +1,289 @@
|
|||||||
"""Micron markup renderer for Ren Browser.
|
"""Micron markup renderer for Ren Browser.
|
||||||
|
|
||||||
Provides rendering capabilities for micron markup content,
|
Provides rendering capabilities for micron markup content.
|
||||||
currently implemented as a placeholder.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
import flet as ft
|
import flet as ft
|
||||||
|
|
||||||
|
from ren_browser.renderer.plaintext import render_plaintext
|
||||||
|
|
||||||
def render_micron(content: str) -> ft.Control:
|
|
||||||
"""Render micron markup content to a Flet control placeholder.
|
|
||||||
|
|
||||||
Currently displays raw content.
|
def hex_to_rgb(hex_color: str) -> str:
|
||||||
|
"""Convert 3-char hex color to RGB string."""
|
||||||
|
if len(hex_color) != 3:
|
||||||
|
return "255,255,255"
|
||||||
|
r = int(hex_color[0], 16) * 17
|
||||||
|
g = int(hex_color[1], 16) * 17
|
||||||
|
b = int(hex_color[2], 16) * 17
|
||||||
|
return f"{r},{g},{b}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_micron_line(line: str) -> list:
|
||||||
|
"""Parse a single line of micron markup into styled text spans.
|
||||||
|
|
||||||
|
Returns list of dicts with 'text', 'bold', 'italic', 'underline', 'color', 'bgcolor'.
|
||||||
|
"""
|
||||||
|
spans = []
|
||||||
|
current_text = ""
|
||||||
|
bold = False
|
||||||
|
italic = False
|
||||||
|
underline = False
|
||||||
|
color = None
|
||||||
|
bgcolor = None
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(line):
|
||||||
|
if line[i] == "`" and i + 1 < len(line):
|
||||||
|
if current_text:
|
||||||
|
spans.append({
|
||||||
|
"text": current_text,
|
||||||
|
"bold": bold,
|
||||||
|
"italic": italic,
|
||||||
|
"underline": underline,
|
||||||
|
"color": color,
|
||||||
|
"bgcolor": bgcolor,
|
||||||
|
})
|
||||||
|
current_text = ""
|
||||||
|
|
||||||
|
tag = line[i + 1]
|
||||||
|
|
||||||
|
if tag == "!":
|
||||||
|
bold = not bold
|
||||||
|
i += 2
|
||||||
|
elif tag == "*":
|
||||||
|
italic = not italic
|
||||||
|
i += 2
|
||||||
|
elif tag == "_":
|
||||||
|
underline = not underline
|
||||||
|
i += 2
|
||||||
|
elif tag == "F" and i + 5 <= len(line):
|
||||||
|
color = hex_to_rgb(line[i+2:i+5])
|
||||||
|
i += 5
|
||||||
|
elif tag == "f":
|
||||||
|
color = None
|
||||||
|
i += 2
|
||||||
|
elif tag == "B" and i + 5 <= len(line):
|
||||||
|
bgcolor = hex_to_rgb(line[i+2:i+5])
|
||||||
|
i += 5
|
||||||
|
elif tag == "b":
|
||||||
|
bgcolor = None
|
||||||
|
i += 2
|
||||||
|
elif tag == "`":
|
||||||
|
bold = False
|
||||||
|
italic = False
|
||||||
|
underline = False
|
||||||
|
color = None
|
||||||
|
bgcolor = None
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
current_text += line[i]
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
current_text += line[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if current_text:
|
||||||
|
spans.append({
|
||||||
|
"text": current_text,
|
||||||
|
"bold": bold,
|
||||||
|
"italic": italic,
|
||||||
|
"underline": underline,
|
||||||
|
"color": color,
|
||||||
|
"bgcolor": bgcolor,
|
||||||
|
})
|
||||||
|
|
||||||
|
return spans
|
||||||
|
|
||||||
|
|
||||||
|
def render_micron(content: str, on_link_click=None) -> ft.Control:
|
||||||
|
"""Render micron markup content to a Flet control.
|
||||||
|
|
||||||
|
Falls back to plaintext renderer if parsing fails.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: Micron markup content to render.
|
content: Micron markup content to render.
|
||||||
|
on_link_click: Optional callback function(url) called when a link is clicked.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ft.Control: Rendered content as a Flet control.
|
ft.Control: Rendered content as a Flet control.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return ft.Text(
|
try:
|
||||||
content,
|
return _render_micron_internal(content, on_link_click)
|
||||||
selectable=True,
|
except Exception as e:
|
||||||
font_family="monospace",
|
print(f"Micron rendering failed: {e}, falling back to plaintext")
|
||||||
|
return render_plaintext(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_micron_internal(content: str, on_link_click=None) -> ft.Control:
|
||||||
|
"""Internal micron rendering implementation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Micron markup content to render.
|
||||||
|
on_link_click: Optional callback function(url) called when a link is clicked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ft.Control: Rendered content as a Flet control.
|
||||||
|
|
||||||
|
"""
|
||||||
|
lines = content.split("\n")
|
||||||
|
controls = []
|
||||||
|
section_level = 0
|
||||||
|
alignment = ft.TextAlign.LEFT
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if not line:
|
||||||
|
controls.append(ft.Container(height=10))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith("`c"):
|
||||||
|
alignment = ft.TextAlign.CENTER
|
||||||
|
line = line[2:]
|
||||||
|
elif line.startswith("`l"):
|
||||||
|
alignment = ft.TextAlign.LEFT
|
||||||
|
line = line[2:]
|
||||||
|
elif line.startswith("`r"):
|
||||||
|
alignment = ft.TextAlign.RIGHT
|
||||||
|
line = line[2:]
|
||||||
|
elif line.startswith("`a"):
|
||||||
|
alignment = ft.TextAlign.LEFT
|
||||||
|
line = line[2:]
|
||||||
|
|
||||||
|
if line.startswith(">"):
|
||||||
|
level = 0
|
||||||
|
while level < len(line) and line[level] == ">":
|
||||||
|
level += 1
|
||||||
|
section_level = level
|
||||||
|
heading_text = line[level:].strip()
|
||||||
|
|
||||||
|
if heading_text:
|
||||||
|
controls.append(
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Text(
|
||||||
|
heading_text,
|
||||||
|
size=20 - (level * 2),
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=ft.Colors.BLUE_400,
|
||||||
|
),
|
||||||
|
padding=ft.padding.only(left=level * 20, top=10, bottom=5),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.strip() == "-":
|
||||||
|
controls.append(
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Divider(color=ft.Colors.GREY_700),
|
||||||
|
padding=ft.padding.only(left=section_level * 20),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "`[" in line:
|
||||||
|
row_controls = []
|
||||||
|
remaining = line
|
||||||
|
last_end = 0
|
||||||
|
|
||||||
|
for link_match in re.finditer(r"`\[([^`]*)`([^\]]*)\]", line):
|
||||||
|
before = line[last_end:link_match.start()]
|
||||||
|
if before:
|
||||||
|
before_spans = parse_micron_line(before)
|
||||||
|
for span in before_spans:
|
||||||
|
row_controls.append(create_text_span(span))
|
||||||
|
|
||||||
|
label = link_match.group(1)
|
||||||
|
url = link_match.group(2)
|
||||||
|
|
||||||
|
def make_link_handler(link_url):
|
||||||
|
def handler(e):
|
||||||
|
if on_link_click:
|
||||||
|
on_link_click(link_url)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
row_controls.append(
|
||||||
|
ft.TextButton(
|
||||||
|
text=label if label else url,
|
||||||
|
style=ft.ButtonStyle(
|
||||||
|
color=ft.Colors.BLUE_400,
|
||||||
|
overlay_color=ft.Colors.BLUE_900,
|
||||||
|
),
|
||||||
|
on_click=make_link_handler(url),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
last_end = link_match.end()
|
||||||
|
|
||||||
|
after = line[last_end:]
|
||||||
|
if after:
|
||||||
|
after_spans = parse_micron_line(after)
|
||||||
|
for span in after_spans:
|
||||||
|
row_controls.append(create_text_span(span))
|
||||||
|
|
||||||
|
if row_controls:
|
||||||
|
controls.append(
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=row_controls,
|
||||||
|
spacing=0,
|
||||||
|
wrap=True,
|
||||||
|
),
|
||||||
|
padding=ft.padding.only(left=section_level * 20),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
spans = parse_micron_line(line)
|
||||||
|
if spans:
|
||||||
|
text_controls = [create_text_span(span) for span in spans]
|
||||||
|
|
||||||
|
controls.append(
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=text_controls,
|
||||||
|
spacing=0,
|
||||||
|
wrap=True,
|
||||||
|
alignment=alignment,
|
||||||
|
),
|
||||||
|
padding=ft.padding.only(left=section_level * 20),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return ft.Column(
|
||||||
|
controls=controls,
|
||||||
|
spacing=5,
|
||||||
|
scroll=ft.ScrollMode.AUTO,
|
||||||
expand=True,
|
expand=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_text_span(span: dict) -> ft.Text:
|
||||||
|
"""Create a Text control from a span dict."""
|
||||||
|
styles = []
|
||||||
|
if span["bold"]:
|
||||||
|
styles.append(ft.TextStyle(weight=ft.FontWeight.BOLD))
|
||||||
|
if span["italic"]:
|
||||||
|
styles.append(ft.TextStyle(italic=True))
|
||||||
|
|
||||||
|
text_decoration = ft.TextDecoration.UNDERLINE if span["underline"] else None
|
||||||
|
color = span["color"]
|
||||||
|
bgcolor = span["bgcolor"]
|
||||||
|
|
||||||
|
text_style = ft.TextStyle(
|
||||||
|
weight=ft.FontWeight.BOLD if span["bold"] else None,
|
||||||
|
italic=span["italic"] if span["italic"] else None,
|
||||||
|
decoration=text_decoration,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ft.Text(
|
||||||
|
span["text"],
|
||||||
|
style=text_style,
|
||||||
|
color=f"rgb({color})" if color else None,
|
||||||
|
bgcolor=f"rgb({bgcolor})" if bgcolor else None,
|
||||||
|
selectable=True,
|
||||||
|
no_wrap=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ and other application data across different platforms.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any
|
||||||
|
|
||||||
import flet as ft
|
import flet as ft
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class StorageManager:
|
|||||||
with platform-specific storage locations.
|
with platform-specific storage locations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, page: Optional[ft.Page] = None):
|
def __init__(self, page: ft.Page | None = None):
|
||||||
"""Initialize storage manager.
|
"""Initialize storage manager.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -45,11 +45,10 @@ class StorageManager:
|
|||||||
else:
|
else:
|
||||||
storage_dir = pathlib.Path("/data/local/tmp/ren_browser")
|
storage_dir = pathlib.Path("/data/local/tmp/ren_browser")
|
||||||
elif hasattr(os, "uname") and "iOS" in str(
|
elif hasattr(os, "uname") and "iOS" in str(
|
||||||
getattr(os, "uname", lambda: "")()
|
getattr(os, "uname", lambda: "")(),
|
||||||
).replace("iPhone", "iOS"):
|
).replace("iPhone", "iOS"):
|
||||||
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
|
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
|
||||||
else:
|
elif "APPDATA" in os.environ: # Windows
|
||||||
if "APPDATA" in os.environ: # Windows
|
|
||||||
storage_dir = pathlib.Path(os.environ["APPDATA"]) / "ren_browser"
|
storage_dir = pathlib.Path(os.environ["APPDATA"]) / "ren_browser"
|
||||||
elif "XDG_CONFIG_HOME" in os.environ: # Linux XDG standard
|
elif "XDG_CONFIG_HOME" in os.environ: # Linux XDG standard
|
||||||
storage_dir = (
|
storage_dir = (
|
||||||
@@ -127,7 +126,7 @@ class StorageManager:
|
|||||||
if self.page and hasattr(self.page, "client_storage"):
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
self.page.client_storage.set("ren_browser_config", config_content)
|
self.page.client_storage.set("ren_browser_config", config_content)
|
||||||
self.page.client_storage.set(
|
self.page.client_storage.set(
|
||||||
"ren_browser_config_error", f"File save failed: {error}"
|
"ren_browser_config_error", f"File save failed: {error}",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -194,7 +193,7 @@ class StorageManager:
|
|||||||
|
|
||||||
if self.page and hasattr(self.page, "client_storage"):
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
self.page.client_storage.set(
|
self.page.client_storage.set(
|
||||||
"ren_browser_bookmarks", json.dumps(bookmarks)
|
"ren_browser_bookmarks", json.dumps(bookmarks),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -206,7 +205,7 @@ class StorageManager:
|
|||||||
try:
|
try:
|
||||||
bookmarks_path = self._storage_dir / "bookmarks.json"
|
bookmarks_path = self._storage_dir / "bookmarks.json"
|
||||||
if bookmarks_path.exists():
|
if bookmarks_path.exists():
|
||||||
with open(bookmarks_path, "r", encoding="utf-8") as f:
|
with open(bookmarks_path, encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
if self.page and hasattr(self.page, "client_storage"):
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
@@ -238,7 +237,7 @@ class StorageManager:
|
|||||||
try:
|
try:
|
||||||
history_path = self._storage_dir / "history.json"
|
history_path = self._storage_dir / "history.json"
|
||||||
if history_path.exists():
|
if history_path.exists():
|
||||||
with open(history_path, "r", encoding="utf-8") as f:
|
with open(history_path, encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
if self.page and hasattr(self.page, "client_storage"):
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
@@ -251,7 +250,46 @@ class StorageManager:
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_storage_info(self) -> Dict[str, Any]:
|
def save_app_settings(self, settings: dict) -> bool:
|
||||||
|
"""Save application settings to storage."""
|
||||||
|
try:
|
||||||
|
settings_path = self._storage_dir / "settings.json"
|
||||||
|
with open(settings_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(settings, f, indent=2)
|
||||||
|
|
||||||
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
|
self.page.client_storage.set("ren_browser_settings", json.dumps(settings))
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def load_app_settings(self) -> dict:
|
||||||
|
"""Load application settings from storage."""
|
||||||
|
default_settings = {
|
||||||
|
"horizontal_scroll": False,
|
||||||
|
"page_bgcolor": "#000000",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
settings_path = self._storage_dir / "settings.json"
|
||||||
|
if settings_path.exists():
|
||||||
|
with open(settings_path, encoding="utf-8") as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
return {**default_settings, **loaded}
|
||||||
|
|
||||||
|
if self.page and hasattr(self.page, "client_storage"):
|
||||||
|
stored_settings = self.page.client_storage.get("ren_browser_settings")
|
||||||
|
if stored_settings and isinstance(stored_settings, str):
|
||||||
|
loaded = json.loads(stored_settings)
|
||||||
|
return {**default_settings, **loaded}
|
||||||
|
|
||||||
|
except (OSError, json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return default_settings
|
||||||
|
|
||||||
|
def get_storage_info(self) -> dict[str, Any]:
|
||||||
"""Get information about the storage system."""
|
"""Get information about the storage system."""
|
||||||
return {
|
return {
|
||||||
"storage_dir": str(self._storage_dir),
|
"storage_dir": str(self._storage_dir),
|
||||||
@@ -275,10 +313,10 @@ class StorageManager:
|
|||||||
|
|
||||||
|
|
||||||
# Global storage instance
|
# Global storage instance
|
||||||
_storage_manager: Optional[StorageManager] = None
|
_storage_manager: StorageManager | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_storage_manager(page: Optional[ft.Page] = None) -> StorageManager:
|
def get_storage_manager(page: ft.Page | None = None) -> StorageManager:
|
||||||
"""Get the global storage manager instance."""
|
"""Get the global storage manager instance."""
|
||||||
global _storage_manager
|
global _storage_manager
|
||||||
if _storage_manager is None:
|
if _storage_manager is None:
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
import flet as ft
|
import flet as ft
|
||||||
|
|
||||||
|
from ren_browser.pages.page_request import PageFetcher, PageRequest
|
||||||
from ren_browser.renderer.micron import render_micron
|
from ren_browser.renderer.micron import render_micron
|
||||||
from ren_browser.renderer.plaintext import render_plaintext
|
from ren_browser.renderer.plaintext import render_plaintext
|
||||||
|
from ren_browser.storage.storage import get_storage_manager
|
||||||
|
|
||||||
|
|
||||||
class TabsManager:
|
class TabsManager:
|
||||||
@@ -30,28 +32,53 @@ class TabsManager:
|
|||||||
self.page = page
|
self.page = page
|
||||||
self.page.on_resize = self._on_resize
|
self.page.on_resize = self._on_resize
|
||||||
self.manager = SimpleNamespace(tabs=[], index=0)
|
self.manager = SimpleNamespace(tabs=[], index=0)
|
||||||
self.tab_bar = ft.Row(
|
|
||||||
spacing=4,
|
storage = get_storage_manager(page)
|
||||||
|
self.settings = storage.load_app_settings()
|
||||||
|
|
||||||
|
self.tab_bar = ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
spacing=6,
|
||||||
|
scroll=ft.ScrollMode.AUTO,
|
||||||
|
),
|
||||||
|
padding=ft.padding.symmetric(horizontal=8, vertical=8),
|
||||||
)
|
)
|
||||||
self.overflow_menu = None
|
self.overflow_menu = None
|
||||||
self.content_container = ft.Container(
|
self.content_container = ft.Container(
|
||||||
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5),
|
expand=True,
|
||||||
|
bgcolor=self.settings.get("page_bgcolor", ft.Colors.BLACK),
|
||||||
|
padding=ft.padding.all(16),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def handle_link_click_home(link_url):
|
||||||
|
if len(self.manager.tabs) > 0:
|
||||||
|
tab = self.manager.tabs[0]
|
||||||
|
full_url = link_url
|
||||||
|
if ":" not in link_url:
|
||||||
|
full_url = f"{link_url}:/page/index.mu"
|
||||||
|
tab["url_field"].value = full_url
|
||||||
|
self._on_tab_go(None, 0)
|
||||||
|
|
||||||
default_content = (
|
default_content = (
|
||||||
render_micron("Welcome to Ren Browser")
|
render_micron("Welcome to Ren Browser", on_link_click=handle_link_click_home)
|
||||||
if app_module.RENDERER == "micron"
|
if app_module.RENDERER == "micron"
|
||||||
else render_plaintext("Welcome to Ren Browser")
|
else render_plaintext("Welcome to Ren Browser")
|
||||||
)
|
)
|
||||||
self._add_tab_internal("Home", default_content)
|
self._add_tab_internal("Home", default_content)
|
||||||
self.add_btn = ft.IconButton(
|
self.add_btn = ft.IconButton(
|
||||||
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click,
|
ft.Icons.ADD,
|
||||||
|
tooltip="New Tab",
|
||||||
|
on_click=self._on_add_click,
|
||||||
|
icon_color=ft.Colors.WHITE,
|
||||||
)
|
)
|
||||||
self.close_btn = ft.IconButton(
|
self.close_btn = ft.IconButton(
|
||||||
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click,
|
ft.Icons.CLOSE,
|
||||||
|
tooltip="Close Tab",
|
||||||
|
on_click=self._on_close_click,
|
||||||
|
icon_color=ft.Colors.WHITE,
|
||||||
)
|
)
|
||||||
self.tab_bar.controls.append(self.add_btn)
|
self.tab_bar.content.controls.append(self.add_btn)
|
||||||
self.tab_bar.controls.append(self.close_btn)
|
self.tab_bar.content.controls.append(self.close_btn)
|
||||||
self.select_tab(0)
|
self.select_tab(0)
|
||||||
self._update_tab_visibility()
|
self._update_tab_visibility()
|
||||||
|
|
||||||
@@ -59,6 +86,30 @@ class TabsManager:
|
|||||||
"""Handle page resize event and update tab visibility."""
|
"""Handle page resize event and update tab visibility."""
|
||||||
self._update_tab_visibility()
|
self._update_tab_visibility()
|
||||||
|
|
||||||
|
def apply_settings(self, settings: dict) -> None:
|
||||||
|
"""Apply appearance settings to the tab manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Dictionary containing appearance settings.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.settings = settings
|
||||||
|
bgcolor = settings.get("page_bgcolor", "#000000")
|
||||||
|
self.content_container.bgcolor = bgcolor
|
||||||
|
|
||||||
|
horizontal_scroll = settings.get("horizontal_scroll", False)
|
||||||
|
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
|
||||||
|
|
||||||
|
for tab in self.manager.tabs:
|
||||||
|
if "content" in tab and hasattr(tab["content"], "scroll"):
|
||||||
|
tab["content"].scroll = scroll_mode
|
||||||
|
if "content_control" in tab and hasattr(tab["content_control"], "scroll"):
|
||||||
|
tab["content_control"].scroll = scroll_mode
|
||||||
|
|
||||||
|
if self.content_container.content:
|
||||||
|
self.content_container.content.update()
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
def _update_tab_visibility(self) -> None:
|
def _update_tab_visibility(self) -> None:
|
||||||
"""Dynamically adjust tab visibility based on page width.
|
"""Dynamically adjust tab visibility based on page width.
|
||||||
|
|
||||||
@@ -67,23 +118,20 @@ class TabsManager:
|
|||||||
if not self.page.width or self.page.width == 0:
|
if not self.page.width or self.page.width == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.overflow_menu and self.overflow_menu in self.tab_bar.controls:
|
if self.overflow_menu and self.overflow_menu in self.tab_bar.content.controls:
|
||||||
self.tab_bar.controls.remove(self.overflow_menu)
|
self.tab_bar.content.controls.remove(self.overflow_menu)
|
||||||
self.overflow_menu = None
|
self.overflow_menu = None
|
||||||
|
|
||||||
"""Estimate available width for tabs (Page width - buttons - padding)."""
|
|
||||||
available_width = self.page.width - 100
|
available_width = self.page.width - 100
|
||||||
|
|
||||||
cumulative_width = 0
|
cumulative_width = 0
|
||||||
visible_tabs_count = 0
|
visible_tabs_count = 0
|
||||||
|
|
||||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
tab_containers = [c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)]
|
||||||
|
|
||||||
for i, tab in enumerate(self.manager.tabs):
|
for i, tab in enumerate(self.manager.tabs):
|
||||||
"""Estimate tab width: (char count * avg char width) + padding + spacing."""
|
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.content.spacing
|
||||||
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.spacing
|
|
||||||
|
|
||||||
"""Always show at least one tab."""
|
|
||||||
if cumulative_width + estimated_width <= available_width or i == 0:
|
if cumulative_width + estimated_width <= available_width or i == 0:
|
||||||
cumulative_width += estimated_width
|
cumulative_width += estimated_width
|
||||||
if i < len(tab_containers):
|
if i < len(tab_containers):
|
||||||
@@ -93,7 +141,6 @@ class TabsManager:
|
|||||||
tab_containers[i].visible = False
|
tab_containers[i].visible = False
|
||||||
|
|
||||||
if len(self.manager.tabs) > visible_tabs_count:
|
if len(self.manager.tabs) > visible_tabs_count:
|
||||||
"""Move extra tabs to overflow menu."""
|
|
||||||
overflow_items = []
|
overflow_items = []
|
||||||
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
||||||
tab_data = self.manager.tabs[i]
|
tab_data = self.manager.tabs[i]
|
||||||
@@ -110,7 +157,7 @@ class TabsManager:
|
|||||||
items=overflow_items,
|
items=overflow_items,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tab_bar.controls.insert(visible_tabs_count, self.overflow_menu)
|
self.tab_bar.content.controls.insert(visible_tabs_count, self.overflow_menu)
|
||||||
|
|
||||||
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
|
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
|
||||||
"""Add a new tab to the manager with the given title and content."""
|
"""Add a new tab to the manager with the given title and content."""
|
||||||
@@ -118,17 +165,28 @@ class TabsManager:
|
|||||||
url_field = ft.TextField(
|
url_field = ft.TextField(
|
||||||
value=title,
|
value=title,
|
||||||
expand=True,
|
expand=True,
|
||||||
text_style=ft.TextStyle(size=12),
|
text_style=ft.TextStyle(size=14),
|
||||||
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8),
|
content_padding=ft.padding.symmetric(horizontal=16, vertical=12),
|
||||||
|
border_radius=24,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
focused_border_color=ft.Colors.BLUE_400,
|
||||||
|
bgcolor=ft.Colors.GREY_800,
|
||||||
|
prefix_icon=ft.Icons.SEARCH,
|
||||||
)
|
)
|
||||||
go_btn = ft.IconButton(
|
go_btn = ft.IconButton(
|
||||||
ft.Icons.OPEN_IN_BROWSER,
|
ft.Icons.ARROW_FORWARD,
|
||||||
tooltip="Load URL",
|
tooltip="Go",
|
||||||
on_click=lambda e, i=idx: self._on_tab_go(e, i),
|
on_click=lambda e, i=idx: self._on_tab_go(e, i),
|
||||||
|
icon_color=ft.Colors.BLUE_400,
|
||||||
|
bgcolor=ft.Colors.BLUE_900,
|
||||||
)
|
)
|
||||||
content_control = content
|
content_control = content
|
||||||
|
horizontal_scroll = self.settings.get("horizontal_scroll", False)
|
||||||
|
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
|
||||||
|
|
||||||
tab_content = ft.Column(
|
tab_content = ft.Column(
|
||||||
expand=True,
|
expand=True,
|
||||||
|
scroll=scroll_mode,
|
||||||
controls=[
|
controls=[
|
||||||
content_control,
|
content_control,
|
||||||
],
|
],
|
||||||
@@ -143,15 +201,26 @@ class TabsManager:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
tab_container = ft.Container(
|
tab_container = ft.Container(
|
||||||
content=ft.Text(title),
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Text(
|
||||||
|
title,
|
||||||
|
size=13,
|
||||||
|
weight=ft.FontWeight.W_500,
|
||||||
|
overflow=ft.TextOverflow.ELLIPSIS,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
spacing=8,
|
||||||
|
),
|
||||||
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
||||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
padding=ft.padding.symmetric(horizontal=16, vertical=10),
|
||||||
border_radius=5,
|
border_radius=8,
|
||||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
bgcolor=ft.Colors.GREY_800,
|
||||||
|
ink=True,
|
||||||
|
width=150,
|
||||||
)
|
)
|
||||||
"""Insert the new tab before the add/close buttons."""
|
insert_pos = max(0, len(self.tab_bar.content.controls) - 2)
|
||||||
insert_pos = max(0, len(self.tab_bar.controls) - 2)
|
self.tab_bar.content.controls.insert(insert_pos, tab_container)
|
||||||
self.tab_bar.controls.insert(insert_pos, tab_container)
|
|
||||||
self._update_tab_visibility()
|
self._update_tab_visibility()
|
||||||
|
|
||||||
def _on_add_click(self, e) -> None: # type: ignore
|
def _on_add_click(self, e) -> None: # type: ignore
|
||||||
@@ -160,8 +229,18 @@ class TabsManager:
|
|||||||
content_text = f"Content for {title}"
|
content_text = f"Content for {title}"
|
||||||
import ren_browser.app as app_module
|
import ren_browser.app as app_module
|
||||||
|
|
||||||
|
new_idx = len(self.manager.tabs)
|
||||||
|
|
||||||
|
def handle_link_click_new(link_url):
|
||||||
|
tab = self.manager.tabs[new_idx]
|
||||||
|
full_url = link_url
|
||||||
|
if ":" not in link_url:
|
||||||
|
full_url = f"{link_url}:/page/index.mu"
|
||||||
|
tab["url_field"].value = full_url
|
||||||
|
self._on_tab_go(None, new_idx)
|
||||||
|
|
||||||
content = (
|
content = (
|
||||||
render_micron(content_text)
|
render_micron(content_text, on_link_click=handle_link_click_new)
|
||||||
if app_module.RENDERER == "micron"
|
if app_module.RENDERER == "micron"
|
||||||
else render_plaintext(content_text)
|
else render_plaintext(content_text)
|
||||||
)
|
)
|
||||||
@@ -175,13 +254,13 @@ class TabsManager:
|
|||||||
return
|
return
|
||||||
idx = self.manager.index
|
idx = self.manager.index
|
||||||
|
|
||||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
tab_containers = [c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)]
|
||||||
control_to_remove = tab_containers[idx]
|
control_to_remove = tab_containers[idx]
|
||||||
|
|
||||||
self.manager.tabs.pop(idx)
|
self.manager.tabs.pop(idx)
|
||||||
self.tab_bar.controls.remove(control_to_remove)
|
self.tab_bar.content.controls.remove(control_to_remove)
|
||||||
|
|
||||||
updated_tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
updated_tab_containers = [c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)]
|
||||||
for i, control in enumerate(updated_tab_containers):
|
for i, control in enumerate(updated_tab_containers):
|
||||||
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
|
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
|
||||||
|
|
||||||
@@ -199,12 +278,14 @@ class TabsManager:
|
|||||||
"""
|
"""
|
||||||
self.manager.index = idx
|
self.manager.index = idx
|
||||||
|
|
||||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
tab_containers = [c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)]
|
||||||
for i, control in enumerate(tab_containers):
|
for i, control in enumerate(tab_containers):
|
||||||
if i == idx:
|
if i == idx:
|
||||||
control.bgcolor = ft.Colors.PRIMARY_CONTAINER
|
control.bgcolor = ft.Colors.BLUE_900
|
||||||
|
control.border = ft.border.all(2, ft.Colors.BLUE_400)
|
||||||
else:
|
else:
|
||||||
control.bgcolor = ft.Colors.SURFACE_CONTAINER_HIGHEST
|
control.bgcolor = ft.Colors.GREY_800
|
||||||
|
control.border = None
|
||||||
|
|
||||||
self.content_container.content = self.manager.tabs[idx]["content"]
|
self.content_container.content = self.manager.tabs[idx]["content"]
|
||||||
self.page.update()
|
self.page.update()
|
||||||
@@ -215,16 +296,68 @@ class TabsManager:
|
|||||||
url = tab["url_field"].value.strip()
|
url = tab["url_field"].value.strip()
|
||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
placeholder_text = f"Loading content for {url}"
|
|
||||||
|
placeholder_text = f"Loading content for {url}..."
|
||||||
import ren_browser.app as app_module
|
import ren_browser.app as app_module
|
||||||
|
|
||||||
new_control = (
|
current_node_hash = None
|
||||||
render_micron(placeholder_text)
|
if ":" in url:
|
||||||
|
current_node_hash = url.split(":")[0]
|
||||||
|
|
||||||
|
def handle_link_click(link_url):
|
||||||
|
full_url = link_url
|
||||||
|
if ":" not in link_url:
|
||||||
|
full_url = f"{link_url}:/page/index.mu"
|
||||||
|
elif link_url.startswith(":/"):
|
||||||
|
if current_node_hash:
|
||||||
|
full_url = f"{current_node_hash}{link_url}"
|
||||||
|
else:
|
||||||
|
full_url = link_url
|
||||||
|
tab["url_field"].value = full_url
|
||||||
|
self._on_tab_go(None, idx)
|
||||||
|
|
||||||
|
placeholder_control = (
|
||||||
|
render_micron(placeholder_text, on_link_click=handle_link_click)
|
||||||
if app_module.RENDERER == "micron"
|
if app_module.RENDERER == "micron"
|
||||||
else render_plaintext(placeholder_text)
|
else render_plaintext(placeholder_text)
|
||||||
)
|
)
|
||||||
|
tab["content_control"] = placeholder_control
|
||||||
|
tab["content"].controls[0] = placeholder_control
|
||||||
|
if self.manager.index == idx:
|
||||||
|
self.content_container.content = tab["content"]
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def fetch_and_update():
|
||||||
|
parts = url.split(":", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
result = f"Error: Invalid URL format. Expected format: hash:/page/path"
|
||||||
|
page_path = ""
|
||||||
|
else:
|
||||||
|
dest_hash = parts[0]
|
||||||
|
page_path = parts[1] if parts[1].startswith("/") else f"/{parts[1]}"
|
||||||
|
|
||||||
|
req = PageRequest(destination_hash=dest_hash, page_path=page_path)
|
||||||
|
page_fetcher = PageFetcher()
|
||||||
|
try:
|
||||||
|
result = page_fetcher.fetch_page(req)
|
||||||
|
except Exception as ex:
|
||||||
|
app_module.log_error(str(ex))
|
||||||
|
result = f"Error: {ex}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
tab = self.manager.tabs[idx]
|
||||||
|
except IndexError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if page_path and page_path.endswith(".mu"):
|
||||||
|
new_control = render_micron(result, on_link_click=handle_link_click)
|
||||||
|
else:
|
||||||
|
new_control = render_plaintext(result)
|
||||||
|
|
||||||
tab["content_control"] = new_control
|
tab["content_control"] = new_control
|
||||||
tab["content"].controls[0] = new_control
|
tab["content"].controls[0] = new_control
|
||||||
if self.manager.index == idx:
|
if self.manager.index == idx:
|
||||||
self.content_container.content = tab["content"]
|
self.content_container.content = tab["content"]
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
|
self.page.run_thread(fetch_and_update)
|
||||||
|
|||||||
@@ -25,50 +25,286 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
config_text = f"Error reading config: {ex}"
|
config_text = f"Error reading config: {ex}"
|
||||||
|
|
||||||
|
app_settings = storage.load_app_settings()
|
||||||
|
|
||||||
config_field = ft.TextField(
|
config_field = ft.TextField(
|
||||||
label="Reticulum config",
|
label="Reticulum Configuration",
|
||||||
value=config_text,
|
value=config_text,
|
||||||
expand=True,
|
expand=True,
|
||||||
multiline=True,
|
multiline=True,
|
||||||
|
min_lines=15,
|
||||||
|
max_lines=20,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
focused_border_color=ft.Colors.BLUE_400,
|
||||||
|
text_style=ft.TextStyle(font_family="monospace", size=12),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
horizontal_scroll_switch = ft.Switch(
|
||||||
|
label="Enable Horizontal Scroll (preserve ASCII art)",
|
||||||
|
value=app_settings.get("horizontal_scroll", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
page_bgcolor_field = ft.TextField(
|
||||||
|
label="Page Background Color (hex)",
|
||||||
|
value=app_settings.get("page_bgcolor", "#000000"),
|
||||||
|
hint_text="#000000",
|
||||||
|
width=200,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
focused_border_color=ft.Colors.BLUE_400,
|
||||||
|
)
|
||||||
|
|
||||||
|
color_preview = ft.Container(
|
||||||
|
width=40,
|
||||||
|
height=40,
|
||||||
|
bgcolor=app_settings.get("page_bgcolor", "#000000"),
|
||||||
|
border_radius=8,
|
||||||
|
border=ft.border.all(1, ft.Colors.GREY_700),
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_bgcolor_change(e):
|
||||||
|
try:
|
||||||
|
color_preview.bgcolor = page_bgcolor_field.value
|
||||||
|
page.update()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
page_bgcolor_field.on_change = on_bgcolor_change
|
||||||
|
|
||||||
def on_save_config(ev):
|
def on_save_config(ev):
|
||||||
try:
|
try:
|
||||||
success = storage.save_config(config_field.value)
|
success = storage.save_config(config_field.value)
|
||||||
if success:
|
if success:
|
||||||
print("Config saved successfully. Please restart the app.")
|
snack = ft.SnackBar(
|
||||||
page.snack_bar = ft.SnackBar(
|
content=ft.Row(
|
||||||
ft.Text("Config saved successfully. Please restart the app."),
|
controls=[
|
||||||
open=True,
|
ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN_400, size=20),
|
||||||
|
ft.Text("Configuration saved! Restart app to apply changes.", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.GREEN_900,
|
||||||
|
duration=3000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
else:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text("Failed to save configuration", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=3000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
except Exception as ex:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text(f"Error: {ex}", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=4000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
def on_save_and_reload_config(ev):
|
||||||
|
try:
|
||||||
|
success = storage.save_config(config_field.value)
|
||||||
|
if not success:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text("Failed to save configuration", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=3000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
return
|
||||||
|
|
||||||
|
loading_snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.ProgressRing(width=16, height=16, stroke_width=2, color=ft.Colors.BLUE_400),
|
||||||
|
ft.Text("Reloading Reticulum...", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.BLUE_900,
|
||||||
|
duration=10000,
|
||||||
|
)
|
||||||
|
page.overlay.append(loading_snack)
|
||||||
|
loading_snack.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
def on_reload_complete(success, error):
|
||||||
|
loading_snack.open = False
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN_400, size=20),
|
||||||
|
ft.Text("Reticulum reloaded successfully!", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.GREEN_900,
|
||||||
|
duration=3000,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("Error saving config: Storage operation failed")
|
snack = ft.SnackBar(
|
||||||
page.snack_bar = ft.SnackBar(
|
content=ft.Row(
|
||||||
ft.Text("Error saving config: Storage operation failed"), open=True
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text(f"Reload failed: {error}", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=4000,
|
||||||
)
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
import ren_browser.app as app_module
|
||||||
|
app_module.reload_reticulum(page, on_reload_complete)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"Error saving config: {ex}")
|
snack = ft.SnackBar(
|
||||||
page.snack_bar = ft.SnackBar(
|
content=ft.Row(
|
||||||
ft.Text(f"Error saving config: {ex}"), open=True
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text(f"Error: {ex}", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=4000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
def on_save_app_settings(ev):
|
||||||
|
try:
|
||||||
|
new_settings = {
|
||||||
|
"horizontal_scroll": horizontal_scroll_switch.value,
|
||||||
|
"page_bgcolor": page_bgcolor_field.value,
|
||||||
|
}
|
||||||
|
success = storage.save_app_settings(new_settings)
|
||||||
|
if success:
|
||||||
|
tab_manager.apply_settings(new_settings)
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN_400, size=20),
|
||||||
|
ft.Text("Appearance settings saved and applied!", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.GREEN_900,
|
||||||
|
duration=2000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
else:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text("Failed to save appearance settings", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=3000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
except Exception as ex:
|
||||||
|
snack = ft.SnackBar(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED_400, size=20),
|
||||||
|
ft.Text(f"Error: {ex}", color=ft.Colors.WHITE),
|
||||||
|
],
|
||||||
|
tight=True,
|
||||||
|
),
|
||||||
|
bgcolor=ft.Colors.RED_900,
|
||||||
|
duration=4000,
|
||||||
|
)
|
||||||
|
page.overlay.append(snack)
|
||||||
|
snack.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
save_btn = ft.ElevatedButton(
|
||||||
|
"Save Configuration",
|
||||||
|
icon=ft.Icons.SAVE,
|
||||||
|
on_click=on_save_config,
|
||||||
|
bgcolor=ft.Colors.BLUE_700,
|
||||||
|
color=ft.Colors.WHITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
save_btn = ft.ElevatedButton("Save Config", on_click=on_save_config)
|
save_reload_btn = ft.ElevatedButton(
|
||||||
|
"Save & Hot Reload",
|
||||||
|
icon=ft.Icons.REFRESH,
|
||||||
|
on_click=on_save_and_reload_config,
|
||||||
|
bgcolor=ft.Colors.GREEN_700,
|
||||||
|
color=ft.Colors.WHITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
save_appearance_btn = ft.ElevatedButton(
|
||||||
|
"Save Appearance",
|
||||||
|
icon=ft.Icons.PALETTE,
|
||||||
|
on_click=on_save_app_settings,
|
||||||
|
bgcolor=ft.Colors.BLUE_700,
|
||||||
|
color=ft.Colors.WHITE,
|
||||||
|
)
|
||||||
error_field = ft.TextField(
|
error_field = ft.TextField(
|
||||||
label="Error Logs",
|
label="Error Logs",
|
||||||
value="",
|
value="",
|
||||||
expand=True,
|
expand=True,
|
||||||
multiline=True,
|
multiline=True,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
|
min_lines=15,
|
||||||
|
max_lines=20,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
text_style=ft.TextStyle(font_family="monospace", size=12),
|
||||||
)
|
)
|
||||||
ret_field = ft.TextField(
|
ret_field = ft.TextField(
|
||||||
label="Reticulum logs",
|
label="Reticulum Logs",
|
||||||
value="",
|
value="",
|
||||||
expand=True,
|
expand=True,
|
||||||
multiline=True,
|
multiline=True,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
|
min_lines=15,
|
||||||
|
max_lines=20,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
text_style=ft.TextStyle(font_family="monospace", size=12),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Storage information for debugging
|
|
||||||
storage_info = storage.get_storage_info()
|
storage_info = storage.get_storage_info()
|
||||||
storage_text = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
|
storage_text = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
|
||||||
storage_field = ft.TextField(
|
storage_field = ft.TextField(
|
||||||
@@ -77,14 +313,39 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
|||||||
expand=True,
|
expand=True,
|
||||||
multiline=True,
|
multiline=True,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
|
min_lines=10,
|
||||||
|
max_lines=15,
|
||||||
|
border_color=ft.Colors.GREY_700,
|
||||||
|
text_style=ft.TextStyle(font_family="monospace", size=12),
|
||||||
)
|
)
|
||||||
|
|
||||||
content_placeholder = ft.Container(expand=True)
|
content_placeholder = ft.Container(expand=True)
|
||||||
|
|
||||||
|
appearance_content = ft.Column(
|
||||||
|
spacing=16,
|
||||||
|
controls=[
|
||||||
|
ft.Text("Appearance Settings", size=18, weight=ft.FontWeight.BOLD),
|
||||||
|
horizontal_scroll_switch,
|
||||||
|
ft.Row(
|
||||||
|
controls=[
|
||||||
|
page_bgcolor_field,
|
||||||
|
color_preview,
|
||||||
|
],
|
||||||
|
alignment=ft.MainAxisAlignment.START,
|
||||||
|
spacing=16,
|
||||||
|
),
|
||||||
|
save_appearance_btn,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def show_config(ev):
|
def show_config(ev):
|
||||||
content_placeholder.content = config_field
|
content_placeholder.content = config_field
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
|
def show_appearance(ev):
|
||||||
|
content_placeholder.content = appearance_content
|
||||||
|
page.update()
|
||||||
|
|
||||||
def show_errors(ev):
|
def show_errors(ev):
|
||||||
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged."
|
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged."
|
||||||
content_placeholder.content = error_field
|
content_placeholder.content = error_field
|
||||||
@@ -98,37 +359,100 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
|||||||
def show_storage_info(ev):
|
def show_storage_info(ev):
|
||||||
storage_info = storage.get_storage_info()
|
storage_info = storage.get_storage_info()
|
||||||
storage_field.value = "\n".join(
|
storage_field.value = "\n".join(
|
||||||
[f"{key}: {value}" for key, value in storage_info.items()]
|
[f"{key}: {value}" for key, value in storage_info.items()],
|
||||||
)
|
)
|
||||||
content_placeholder.content = storage_field
|
content_placeholder.content = storage_field
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
def refresh_current_view(ev):
|
def refresh_current_view(ev):
|
||||||
# Refresh the currently displayed content
|
|
||||||
if content_placeholder.content == error_field:
|
if content_placeholder.content == error_field:
|
||||||
show_errors(ev)
|
show_errors(ev)
|
||||||
elif content_placeholder.content == ret_field:
|
elif content_placeholder.content == ret_field:
|
||||||
show_ret_logs(ev)
|
show_ret_logs(ev)
|
||||||
elif content_placeholder.content == storage_field:
|
elif content_placeholder.content == storage_field:
|
||||||
show_storage_info(ev)
|
show_storage_info(ev)
|
||||||
|
elif content_placeholder.content == appearance_content:
|
||||||
|
show_appearance(ev)
|
||||||
elif content_placeholder.content == config_field:
|
elif content_placeholder.content == config_field:
|
||||||
show_config(ev)
|
show_config(ev)
|
||||||
|
|
||||||
btn_config = ft.ElevatedButton("Config", on_click=show_config)
|
btn_config = ft.FilledButton(
|
||||||
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors)
|
"Configuration",
|
||||||
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs)
|
icon=ft.Icons.SETTINGS,
|
||||||
btn_storage = ft.ElevatedButton("Storage", on_click=show_storage_info)
|
on_click=show_config,
|
||||||
btn_refresh = ft.ElevatedButton("Refresh", on_click=refresh_current_view)
|
|
||||||
button_row = ft.Row(
|
|
||||||
controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh]
|
|
||||||
)
|
)
|
||||||
|
btn_appearance = ft.FilledButton(
|
||||||
|
"Appearance",
|
||||||
|
icon=ft.Icons.PALETTE,
|
||||||
|
on_click=show_appearance,
|
||||||
|
)
|
||||||
|
btn_errors = ft.FilledButton(
|
||||||
|
"Errors",
|
||||||
|
icon=ft.Icons.ERROR_OUTLINE,
|
||||||
|
on_click=show_errors,
|
||||||
|
)
|
||||||
|
btn_ret = ft.FilledButton(
|
||||||
|
"Reticulum Logs",
|
||||||
|
icon=ft.Icons.TERMINAL,
|
||||||
|
on_click=show_ret_logs,
|
||||||
|
)
|
||||||
|
btn_storage = ft.FilledButton(
|
||||||
|
"Storage",
|
||||||
|
icon=ft.Icons.STORAGE,
|
||||||
|
on_click=show_storage_info,
|
||||||
|
)
|
||||||
|
btn_refresh = ft.IconButton(
|
||||||
|
icon=ft.Icons.REFRESH,
|
||||||
|
tooltip="Refresh",
|
||||||
|
on_click=refresh_current_view,
|
||||||
|
icon_color=ft.Colors.BLUE_400,
|
||||||
|
)
|
||||||
|
|
||||||
|
nav_card = ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[btn_config, btn_appearance, btn_errors, btn_ret, btn_storage, btn_refresh],
|
||||||
|
spacing=8,
|
||||||
|
wrap=True,
|
||||||
|
),
|
||||||
|
padding=ft.padding.all(16),
|
||||||
|
border_radius=12,
|
||||||
|
bgcolor=ft.Colors.GREY_900,
|
||||||
|
)
|
||||||
|
|
||||||
|
content_card = ft.Container(
|
||||||
|
content=content_placeholder,
|
||||||
|
expand=True,
|
||||||
|
padding=ft.padding.all(16),
|
||||||
|
border_radius=12,
|
||||||
|
bgcolor=ft.Colors.GREY_900,
|
||||||
|
)
|
||||||
|
|
||||||
|
action_row = ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[save_btn, save_reload_btn],
|
||||||
|
alignment=ft.MainAxisAlignment.END,
|
||||||
|
spacing=8,
|
||||||
|
),
|
||||||
|
padding=ft.padding.symmetric(horizontal=16, vertical=8),
|
||||||
|
)
|
||||||
|
|
||||||
content_placeholder.content = config_field
|
content_placeholder.content = config_field
|
||||||
settings_content = ft.Column(
|
settings_content = ft.Column(
|
||||||
expand=True,
|
expand=True,
|
||||||
|
spacing=16,
|
||||||
controls=[
|
controls=[
|
||||||
button_row,
|
ft.Container(
|
||||||
content_placeholder,
|
content=ft.Text(
|
||||||
ft.Row([save_btn]),
|
"Settings",
|
||||||
|
size=24,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=ft.Colors.BLUE_400,
|
||||||
|
),
|
||||||
|
padding=ft.padding.only(left=16, top=16),
|
||||||
|
),
|
||||||
|
nav_card,
|
||||||
|
content_card,
|
||||||
|
action_row,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
tab_manager._add_tab_internal("Settings", settings_content)
|
tab_manager._add_tab_internal("Settings", settings_content)
|
||||||
|
|||||||
@@ -23,11 +23,26 @@ def build_ui(page: Page):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
page.theme_mode = ft.ThemeMode.DARK
|
page.theme_mode = ft.ThemeMode.DARK
|
||||||
page.appbar = ft.AppBar()
|
page.theme = ft.Theme(
|
||||||
|
color_scheme=ft.ColorScheme(
|
||||||
|
primary=ft.Colors.BLUE_400,
|
||||||
|
on_primary=ft.Colors.WHITE,
|
||||||
|
surface=ft.Colors.BLACK,
|
||||||
|
on_surface=ft.Colors.WHITE,
|
||||||
|
background=ft.Colors.BLACK,
|
||||||
|
on_background=ft.Colors.WHITE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
page.bgcolor = ft.Colors.BLACK
|
||||||
|
page.appbar = ft.AppBar(
|
||||||
|
bgcolor=ft.Colors.GREY_900,
|
||||||
|
elevation=2,
|
||||||
|
)
|
||||||
page.window.maximized = True
|
page.window.maximized = True
|
||||||
|
page.padding = 0
|
||||||
|
|
||||||
page_fetcher = PageFetcher()
|
page_fetcher = PageFetcher()
|
||||||
announce_list = ft.ListView(expand=True, spacing=1)
|
announce_list = ft.ListView(expand=True, spacing=8, padding=ft.padding.all(8))
|
||||||
|
|
||||||
def update_announces(ann_list):
|
def update_announces(ann_list):
|
||||||
announce_list.controls.clear()
|
announce_list.controls.clear()
|
||||||
@@ -58,8 +73,18 @@ def build_ui(page: Page):
|
|||||||
tab = tab_manager.manager.tabs[idx]
|
tab = tab_manager.manager.tabs[idx]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def handle_link_click(url):
|
||||||
|
full_url = url
|
||||||
|
if ":" not in url:
|
||||||
|
full_url = f"{url}:/page/index.mu"
|
||||||
|
elif url.startswith(":/"):
|
||||||
|
full_url = f"{dest}{url}"
|
||||||
|
tab["url_field"].value = full_url
|
||||||
|
tab_manager._on_tab_go(None, idx)
|
||||||
|
|
||||||
if req.page_path.endswith(".mu"):
|
if req.page_path.endswith(".mu"):
|
||||||
new_control = render_micron(result)
|
new_control = render_micron(result, on_link_click=handle_link_click)
|
||||||
else:
|
else:
|
||||||
new_control = render_plaintext(result)
|
new_control = render_plaintext(result)
|
||||||
tab["content_control"] = new_control
|
tab["content_control"] = new_control
|
||||||
@@ -70,25 +95,50 @@ def build_ui(page: Page):
|
|||||||
|
|
||||||
page.run_thread(fetch_and_update)
|
page.run_thread(fetch_and_update)
|
||||||
|
|
||||||
announce_list.controls.append(ft.TextButton(label, on_click=on_click_ann))
|
announce_card = ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.LANGUAGE, size=20, color=ft.Colors.BLUE_400),
|
||||||
|
ft.Text(
|
||||||
|
label,
|
||||||
|
size=14,
|
||||||
|
weight=ft.FontWeight.W_500,
|
||||||
|
overflow=ft.TextOverflow.ELLIPSIS,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
spacing=12,
|
||||||
|
),
|
||||||
|
padding=ft.padding.all(12),
|
||||||
|
border_radius=8,
|
||||||
|
bgcolor=ft.Colors.GREY_800,
|
||||||
|
ink=True,
|
||||||
|
on_click=on_click_ann,
|
||||||
|
)
|
||||||
|
announce_list.controls.append(announce_card)
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
AnnounceService(update_callback=update_announces)
|
AnnounceService(update_callback=update_announces)
|
||||||
page.drawer = ft.NavigationDrawer(
|
page.drawer = ft.NavigationDrawer(
|
||||||
|
bgcolor=ft.Colors.GREY_900,
|
||||||
|
elevation=8,
|
||||||
controls=[
|
controls=[
|
||||||
ft.Text(
|
ft.Container(
|
||||||
|
content=ft.Text(
|
||||||
"Announcements",
|
"Announcements",
|
||||||
|
size=20,
|
||||||
weight=ft.FontWeight.BOLD,
|
weight=ft.FontWeight.BOLD,
|
||||||
text_align=ft.TextAlign.CENTER,
|
color=ft.Colors.BLUE_400,
|
||||||
expand=True,
|
|
||||||
),
|
),
|
||||||
ft.Divider(),
|
padding=ft.padding.symmetric(horizontal=16, vertical=20),
|
||||||
|
),
|
||||||
|
ft.Divider(height=1, color=ft.Colors.GREY_700),
|
||||||
announce_list,
|
announce_list,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
page.appbar.leading = ft.IconButton(
|
page.appbar.leading = ft.IconButton(
|
||||||
ft.Icons.MENU,
|
ft.Icons.MENU,
|
||||||
tooltip="Toggle sidebar",
|
tooltip="Announcements",
|
||||||
|
icon_color=ft.Colors.WHITE,
|
||||||
on_click=lambda e: (
|
on_click=lambda e: (
|
||||||
setattr(page.drawer, "open", not page.drawer.open),
|
setattr(page.drawer, "open", not page.drawer.open),
|
||||||
page.update(),
|
page.update(),
|
||||||
@@ -102,15 +152,21 @@ def build_ui(page: Page):
|
|||||||
ft.IconButton(
|
ft.IconButton(
|
||||||
ft.Icons.SETTINGS,
|
ft.Icons.SETTINGS,
|
||||||
tooltip="Settings",
|
tooltip="Settings",
|
||||||
|
icon_color=ft.Colors.WHITE,
|
||||||
on_click=lambda e: open_settings_tab(page, tab_manager),
|
on_click=lambda e: open_settings_tab(page, tab_manager),
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
Shortcuts(page, tab_manager)
|
Shortcuts(page, tab_manager)
|
||||||
url_bar = ft.Row(
|
url_bar = ft.Container(
|
||||||
|
content=ft.Row(
|
||||||
controls=[
|
controls=[
|
||||||
tab_manager.manager.tabs[tab_manager.manager.index]["url_field"],
|
tab_manager.manager.tabs[tab_manager.manager.index]["url_field"],
|
||||||
tab_manager.manager.tabs[tab_manager.manager.index]["go_btn"],
|
tab_manager.manager.tabs[tab_manager.manager.index]["go_btn"],
|
||||||
],
|
],
|
||||||
|
spacing=8,
|
||||||
|
),
|
||||||
|
expand=True,
|
||||||
|
padding=ft.padding.symmetric(horizontal=8),
|
||||||
)
|
)
|
||||||
page.appbar.title = url_bar
|
page.appbar.title = url_bar
|
||||||
orig_select_tab = tab_manager.select_tab
|
orig_select_tab = tab_manager.select_tab
|
||||||
@@ -118,8 +174,8 @@ def build_ui(page: Page):
|
|||||||
def _select_tab_and_update_url(i):
|
def _select_tab_and_update_url(i):
|
||||||
orig_select_tab(i)
|
orig_select_tab(i)
|
||||||
tab = tab_manager.manager.tabs[i]
|
tab = tab_manager.manager.tabs[i]
|
||||||
url_bar.controls.clear()
|
url_bar.content.controls.clear()
|
||||||
url_bar.controls.extend([tab["url_field"], tab["go_btn"]])
|
url_bar.content.controls.extend([tab["url_field"], tab["go_btn"]])
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
tab_manager.select_tab = _select_tab_and_update_url
|
tab_manager.select_tab = _select_tab_and_update_url
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def sample_page_request():
|
|||||||
from ren_browser.pages.page_request import PageRequest
|
from ren_browser.pages.page_request import PageRequest
|
||||||
|
|
||||||
return PageRequest(
|
return PageRequest(
|
||||||
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None
|
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class TestAnnounce:
|
|||||||
def test_announce_with_none_display_name(self):
|
def test_announce_with_none_display_name(self):
|
||||||
"""Test Announce creation with None display name."""
|
"""Test Announce creation with None display name."""
|
||||||
announce = Announce(
|
announce = Announce(
|
||||||
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890
|
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert announce.destination_hash == "1234567890abcdef"
|
assert announce.destination_hash == "1234567890abcdef"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class TestLogsModule:
|
|||||||
assert len(logs.RET_LOGS) == 1
|
assert len(logs.RET_LOGS) == 1
|
||||||
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
|
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
|
||||||
logs._original_rns_log.assert_called_once_with(
|
logs._original_rns_log.assert_called_once_with(
|
||||||
"Test RNS message", "arg1", kwarg1="value1"
|
"Test RNS message", "arg1", kwarg1="value1",
|
||||||
)
|
)
|
||||||
assert result == "original_result"
|
assert result == "original_result"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class TestPageRequest:
|
|||||||
def test_page_request_creation(self):
|
def test_page_request_creation(self):
|
||||||
"""Test basic PageRequest creation."""
|
"""Test basic PageRequest creation."""
|
||||||
request = PageRequest(
|
request = PageRequest(
|
||||||
destination_hash="1234567890abcdef", page_path="/page/index.mu"
|
destination_hash="1234567890abcdef", page_path="/page/index.mu",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert request.destination_hash == "1234567890abcdef"
|
assert request.destination_hash == "1234567890abcdef"
|
||||||
|
|||||||
@@ -63,66 +63,58 @@ class TestMicronRenderer:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def test_render_micron_basic(self):
|
def test_render_micron_basic(self):
|
||||||
"""Test basic micron rendering (currently displays raw content)."""
|
"""Test basic micron rendering."""
|
||||||
content = "# Heading\n\nSome content"
|
content = "# Heading\n\nSome content"
|
||||||
result = render_micron(content)
|
result = render_micron(content)
|
||||||
|
|
||||||
assert isinstance(result, ft.Text)
|
assert isinstance(result, ft.Column)
|
||||||
assert result.value == "# Heading\n\nSome content"
|
|
||||||
assert result.selectable is True
|
|
||||||
assert result.font_family == "monospace"
|
|
||||||
assert result.expand is True
|
assert result.expand is True
|
||||||
|
assert result.scroll == ft.ScrollMode.AUTO
|
||||||
|
|
||||||
def test_render_micron_empty(self):
|
def test_render_micron_empty(self):
|
||||||
"""Test micron rendering with empty content."""
|
"""Test micron rendering with empty content."""
|
||||||
content = ""
|
content = ""
|
||||||
result = render_micron(content)
|
result = render_micron(content)
|
||||||
|
|
||||||
assert isinstance(result, ft.Text)
|
assert isinstance(result, ft.Column)
|
||||||
assert result.value == ""
|
assert len(result.controls) >= 0
|
||||||
assert result.selectable is True
|
|
||||||
|
|
||||||
def test_render_micron_unicode(self):
|
def test_render_micron_unicode(self):
|
||||||
"""Test micron rendering with Unicode characters."""
|
"""Test micron rendering with Unicode characters."""
|
||||||
content = "Unicode content: 你好 🌍 αβγ"
|
content = "Unicode content: 你好 🌍 αβγ"
|
||||||
result = render_micron(content)
|
result = render_micron(content)
|
||||||
|
|
||||||
assert isinstance(result, ft.Text)
|
assert isinstance(result, ft.Column)
|
||||||
assert result.value == content
|
assert len(result.controls) > 0
|
||||||
assert result.selectable is True
|
|
||||||
|
|
||||||
|
|
||||||
class TestRendererComparison:
|
class TestRendererComparison:
|
||||||
"""Test cases comparing both renderers."""
|
"""Test cases comparing both renderers."""
|
||||||
|
|
||||||
def test_renderers_return_same_type(self):
|
def test_renderers_return_same_type(self):
|
||||||
"""Test that both renderers return the same control type."""
|
"""Test that both renderers return Flet controls."""
|
||||||
content = "Test content"
|
content = "Test content"
|
||||||
|
|
||||||
plaintext_result = render_plaintext(content)
|
plaintext_result = render_plaintext(content)
|
||||||
micron_result = render_micron(content)
|
micron_result = render_micron(content)
|
||||||
|
|
||||||
assert type(plaintext_result) is type(micron_result)
|
|
||||||
assert isinstance(plaintext_result, ft.Text)
|
assert isinstance(plaintext_result, ft.Text)
|
||||||
assert isinstance(micron_result, ft.Text)
|
assert isinstance(micron_result, ft.Column)
|
||||||
|
|
||||||
def test_renderers_preserve_content(self):
|
def test_renderers_preserve_content(self):
|
||||||
"""Test that both renderers preserve the original content."""
|
"""Test that plaintext renderer preserves content."""
|
||||||
content = "Test content with\nmultiple lines"
|
content = "Test content with\nmultiple lines"
|
||||||
|
|
||||||
plaintext_result = render_plaintext(content)
|
plaintext_result = render_plaintext(content)
|
||||||
micron_result = render_micron(content)
|
|
||||||
|
|
||||||
assert plaintext_result.value == content
|
assert plaintext_result.value == content
|
||||||
assert micron_result.value == content
|
|
||||||
|
|
||||||
def test_renderers_same_properties(self):
|
def test_renderers_same_properties(self):
|
||||||
"""Test that both renderers set the same basic properties."""
|
"""Test that both renderers have expand property."""
|
||||||
content = "Test content"
|
content = "Test content"
|
||||||
|
|
||||||
plaintext_result = render_plaintext(content)
|
plaintext_result = render_plaintext(content)
|
||||||
micron_result = render_micron(content)
|
micron_result = render_micron(content)
|
||||||
|
|
||||||
assert plaintext_result.selectable == micron_result.selectable
|
assert plaintext_result.expand is True
|
||||||
assert plaintext_result.font_family == micron_result.font_family
|
assert micron_result.expand is True
|
||||||
assert plaintext_result.expand == micron_result.expand
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestStorageManager:
|
|||||||
def test_storage_manager_init_without_page(self):
|
def test_storage_manager_init_without_page(self):
|
||||||
"""Test StorageManager initialization without a page."""
|
"""Test StorageManager initialization without a page."""
|
||||||
with patch(
|
with patch(
|
||||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
"ren_browser.storage.storage.StorageManager._get_storage_directory",
|
||||||
) as mock_get_dir:
|
) as mock_get_dir:
|
||||||
mock_dir = Path("/mock/storage")
|
mock_dir = Path("/mock/storage")
|
||||||
mock_get_dir.return_value = mock_dir
|
mock_get_dir.return_value = mock_dir
|
||||||
@@ -35,7 +35,7 @@ class TestStorageManager:
|
|||||||
mock_page = Mock()
|
mock_page = Mock()
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
"ren_browser.storage.storage.StorageManager._get_storage_directory",
|
||||||
) as mock_get_dir:
|
) as mock_get_dir:
|
||||||
mock_dir = Path("/mock/storage")
|
mock_dir = Path("/mock/storage")
|
||||||
mock_get_dir.return_value = mock_dir
|
mock_get_dir.return_value = mock_dir
|
||||||
@@ -51,12 +51,11 @@ class TestStorageManager:
|
|||||||
with (
|
with (
|
||||||
patch("os.name", "posix"),
|
patch("os.name", "posix"),
|
||||||
patch.dict(
|
patch.dict(
|
||||||
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
|
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True,
|
||||||
|
),
|
||||||
|
patch("pathlib.Path.mkdir"),patch(
|
||||||
|
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||||
),
|
),
|
||||||
patch("pathlib.Path.mkdir"),
|
|
||||||
):
|
|
||||||
with patch(
|
|
||||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
|
||||||
):
|
):
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
storage._storage_dir = storage._get_storage_directory()
|
storage._storage_dir = storage._get_storage_directory()
|
||||||
@@ -76,7 +75,7 @@ class TestStorageManager:
|
|||||||
patch("pathlib.Path.mkdir"),
|
patch("pathlib.Path.mkdir"),
|
||||||
):
|
):
|
||||||
with patch(
|
with patch(
|
||||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||||
):
|
):
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
storage._storage_dir = storage._get_storage_directory()
|
storage._storage_dir = storage._get_storage_directory()
|
||||||
@@ -91,7 +90,7 @@ class TestStorageManager:
|
|||||||
patch("pathlib.Path.mkdir"),
|
patch("pathlib.Path.mkdir"),
|
||||||
):
|
):
|
||||||
with patch(
|
with patch(
|
||||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||||
):
|
):
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
storage._storage_dir = storage._get_storage_directory()
|
storage._storage_dir = storage._get_storage_directory()
|
||||||
@@ -103,10 +102,9 @@ class TestStorageManager:
|
|||||||
with (
|
with (
|
||||||
patch("os.name", "posix"),
|
patch("os.name", "posix"),
|
||||||
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
|
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
|
||||||
patch("pathlib.Path.mkdir"),
|
patch("pathlib.Path.mkdir"),patch(
|
||||||
):
|
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
|
||||||
with patch(
|
),
|
||||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
|
||||||
):
|
):
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
storage._storage_dir = storage._get_storage_directory()
|
storage._storage_dir = storage._get_storage_directory()
|
||||||
@@ -171,7 +169,7 @@ class TestStorageManager:
|
|||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
mock_page.client_storage.set.assert_called_with(
|
mock_page.client_storage.set.assert_called_with(
|
||||||
"ren_browser_config", config_content
|
"ren_browser_config", config_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_save_config_fallback(self):
|
def test_save_config_fallback(self):
|
||||||
@@ -188,8 +186,7 @@ class TestStorageManager:
|
|||||||
storage,
|
storage,
|
||||||
"get_reticulum_config_path",
|
"get_reticulum_config_path",
|
||||||
return_value=Path(temp_dir) / "reticulum",
|
return_value=Path(temp_dir) / "reticulum",
|
||||||
):
|
), patch(
|
||||||
with patch(
|
|
||||||
"pathlib.Path.write_text",
|
"pathlib.Path.write_text",
|
||||||
side_effect=PermissionError("Access denied"),
|
side_effect=PermissionError("Access denied"),
|
||||||
):
|
):
|
||||||
@@ -199,7 +196,7 @@ class TestStorageManager:
|
|||||||
assert result is True
|
assert result is True
|
||||||
# Check that the config was set to client storage
|
# Check that the config was set to client storage
|
||||||
mock_page.client_storage.set.assert_any_call(
|
mock_page.client_storage.set.assert_any_call(
|
||||||
"ren_browser_config", config_content
|
"ren_browser_config", config_content,
|
||||||
)
|
)
|
||||||
# Verify that client storage was called at least once
|
# Verify that client storage was called at least once
|
||||||
assert mock_page.client_storage.set.call_count >= 1
|
assert mock_page.client_storage.set.call_count >= 1
|
||||||
@@ -270,7 +267,7 @@ class TestStorageManager:
|
|||||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||||
assert bookmarks_path.exists()
|
assert bookmarks_path.exists()
|
||||||
|
|
||||||
with open(bookmarks_path, "r", encoding="utf-8") as f:
|
with open(bookmarks_path, encoding="utf-8") as f:
|
||||||
loaded_bookmarks = json.load(f)
|
loaded_bookmarks = json.load(f)
|
||||||
assert loaded_bookmarks == bookmarks
|
assert loaded_bookmarks == bookmarks
|
||||||
|
|
||||||
@@ -311,7 +308,7 @@ class TestStorageManager:
|
|||||||
history_path = storage._storage_dir / "history.json"
|
history_path = storage._storage_dir / "history.json"
|
||||||
assert history_path.exists()
|
assert history_path.exists()
|
||||||
|
|
||||||
with open(history_path, "r", encoding="utf-8") as f:
|
with open(history_path, encoding="utf-8") as f:
|
||||||
loaded_history = json.load(f)
|
loaded_history = json.load(f)
|
||||||
assert loaded_history == history
|
assert loaded_history == history
|
||||||
|
|
||||||
@@ -360,8 +357,7 @@ class TestStorageManager:
|
|||||||
with patch(
|
with patch(
|
||||||
"pathlib.Path.mkdir",
|
"pathlib.Path.mkdir",
|
||||||
side_effect=[PermissionError("Access denied"), None],
|
side_effect=[PermissionError("Access denied"), None],
|
||||||
):
|
), patch("tempfile.gettempdir", return_value="/tmp"):
|
||||||
with patch("tempfile.gettempdir", return_value="/tmp"):
|
|
||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
|
|
||||||
expected_fallback = Path("/tmp") / "ren_browser"
|
expected_fallback = Path("/tmp") / "ren_browser"
|
||||||
@@ -448,7 +444,7 @@ class TestStorageManagerEdgeCases:
|
|||||||
storage = StorageManager()
|
storage = StorageManager()
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
|
"pathlib.Path.write_text", side_effect=PermissionError("Access denied"),
|
||||||
):
|
):
|
||||||
test_path = Path("/mock/path")
|
test_path = Path("/mock/path")
|
||||||
result = storage._is_writable(test_path)
|
result = storage._is_writable(test_path)
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class TestTabsManager:
|
|||||||
assert isinstance(manager.manager, SimpleNamespace)
|
assert isinstance(manager.manager, SimpleNamespace)
|
||||||
assert len(manager.manager.tabs) == 1
|
assert len(manager.manager.tabs) == 1
|
||||||
assert manager.manager.index == 0
|
assert manager.manager.index == 0
|
||||||
assert isinstance(manager.tab_bar, ft.Row)
|
assert isinstance(manager.tab_bar, ft.Container)
|
||||||
assert manager.tab_bar.scroll is None
|
assert isinstance(manager.tab_bar.content, ft.Row)
|
||||||
assert manager.overflow_menu is None
|
assert manager.overflow_menu is None
|
||||||
assert isinstance(manager.content_container, ft.Container)
|
assert isinstance(manager.content_container, ft.Container)
|
||||||
|
|
||||||
@@ -105,12 +105,12 @@ class TestTabsManager:
|
|||||||
"""Test that selecting a tab updates background colors correctly."""
|
"""Test that selecting a tab updates background colors correctly."""
|
||||||
tabs_manager._add_tab_internal("Tab 2", Mock())
|
tabs_manager._add_tab_internal("Tab 2", Mock())
|
||||||
|
|
||||||
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
|
tab_controls = tabs_manager.tab_bar.content.controls[:-2] # Exclude add/close buttons
|
||||||
|
|
||||||
tabs_manager.select_tab(1)
|
tabs_manager.select_tab(1)
|
||||||
|
|
||||||
assert tab_controls[0].bgcolor == ft.Colors.SURFACE_CONTAINER_HIGHEST
|
assert tab_controls[0].bgcolor == ft.Colors.GREY_800
|
||||||
assert tab_controls[1].bgcolor == ft.Colors.PRIMARY_CONTAINER
|
assert tab_controls[1].bgcolor == ft.Colors.BLUE_900
|
||||||
|
|
||||||
def test_on_tab_go_empty_url(self, tabs_manager):
|
def test_on_tab_go_empty_url(self, tabs_manager):
|
||||||
"""Test tab go with empty URL."""
|
"""Test tab go with empty URL."""
|
||||||
@@ -146,12 +146,12 @@ class TestTabsManager:
|
|||||||
def test_tab_container_properties(self, tabs_manager):
|
def test_tab_container_properties(self, tabs_manager):
|
||||||
"""Test that tab container has correct properties."""
|
"""Test that tab container has correct properties."""
|
||||||
assert tabs_manager.content_container.expand is True
|
assert tabs_manager.content_container.expand is True
|
||||||
assert tabs_manager.content_container.bgcolor == ft.Colors.BLACK
|
assert tabs_manager.content_container.bgcolor in (ft.Colors.BLACK, "#000000")
|
||||||
assert tabs_manager.content_container.padding == ft.padding.all(5)
|
assert tabs_manager.content_container.padding == ft.padding.all(16)
|
||||||
|
|
||||||
def test_tab_bar_controls(self, tabs_manager):
|
def test_tab_bar_controls(self, tabs_manager):
|
||||||
"""Test that tab bar has correct controls."""
|
"""Test that tab bar has correct controls."""
|
||||||
controls = tabs_manager.tab_bar.controls
|
controls = tabs_manager.tab_bar.content.controls
|
||||||
|
|
||||||
# Should have: home tab, add button, close button (and potentially overflow menu)
|
# Should have: home tab, add button, close button (and potentially overflow menu)
|
||||||
assert len(controls) >= 3
|
assert len(controls) >= 3
|
||||||
@@ -180,7 +180,7 @@ class TestTabsManager:
|
|||||||
url_field = tab["url_field"]
|
url_field = tab["url_field"]
|
||||||
|
|
||||||
assert url_field.expand is True
|
assert url_field.expand is True
|
||||||
assert url_field.text_style.size == 12
|
assert url_field.text_style.size == 14
|
||||||
assert url_field.content_padding is not None
|
assert url_field.content_padding is not None
|
||||||
|
|
||||||
def test_go_button_properties(self, tabs_manager):
|
def test_go_button_properties(self, tabs_manager):
|
||||||
@@ -188,14 +188,14 @@ class TestTabsManager:
|
|||||||
tab = tabs_manager.manager.tabs[0]
|
tab = tabs_manager.manager.tabs[0]
|
||||||
go_btn = tab["go_btn"]
|
go_btn = tab["go_btn"]
|
||||||
|
|
||||||
assert go_btn.icon == ft.Icons.OPEN_IN_BROWSER
|
assert go_btn.icon == ft.Icons.ARROW_FORWARD
|
||||||
assert go_btn.tooltip == "Load URL"
|
assert go_btn.tooltip == "Go"
|
||||||
|
|
||||||
def test_tab_click_handlers(self, tabs_manager):
|
def test_tab_click_handlers(self, tabs_manager):
|
||||||
"""Test that tab click handlers are properly set."""
|
"""Test that tab click handlers are properly set."""
|
||||||
tabs_manager._add_tab_internal("Tab 2", Mock())
|
tabs_manager._add_tab_internal("Tab 2", Mock())
|
||||||
|
|
||||||
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
|
tab_controls = tabs_manager.tab_bar.content.controls[:-2] # Exclude add/close buttons
|
||||||
|
|
||||||
for i, control in enumerate(tab_controls):
|
for i, control in enumerate(tab_controls):
|
||||||
assert control.on_click is not None
|
assert control.on_click is not None
|
||||||
@@ -249,13 +249,13 @@ class TestTabsManager:
|
|||||||
# Simulate a smaller screen, expecting more tabs to overflow
|
# Simulate a smaller screen, expecting more tabs to overflow
|
||||||
tabs_manager.page.width = 400
|
tabs_manager.page.width = 400
|
||||||
tabs_manager._update_tab_visibility()
|
tabs_manager._update_tab_visibility()
|
||||||
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
|
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.content.controls if isinstance(c, ft.Container) and c.visible)
|
||||||
assert visible_tabs_small < 11
|
assert visible_tabs_small < 11
|
||||||
|
|
||||||
# Simulate a larger screen, expecting all tabs to be visible
|
# Simulate a larger screen, expecting all tabs to be visible
|
||||||
tabs_manager.page.width = 1600
|
tabs_manager.page.width = 1600
|
||||||
tabs_manager._update_tab_visibility()
|
tabs_manager._update_tab_visibility()
|
||||||
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
|
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.content.controls if isinstance(c, ft.Container) and c.visible)
|
||||||
|
|
||||||
assert visible_tabs_large == 11
|
assert visible_tabs_large == 11
|
||||||
assert tabs_manager.overflow_menu is None
|
assert tabs_manager.overflow_menu is None
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TestBuildUI:
|
|||||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||||
def test_build_ui_appbar_setup(
|
def test_build_ui_appbar_setup(
|
||||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
|
||||||
):
|
):
|
||||||
"""Test that build_ui sets up the app bar correctly."""
|
"""Test that build_ui sets up the app bar correctly."""
|
||||||
mock_tab_manager = Mock()
|
mock_tab_manager = Mock()
|
||||||
@@ -51,7 +51,7 @@ class TestBuildUI:
|
|||||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||||
def test_build_ui_drawer_setup(
|
def test_build_ui_drawer_setup(
|
||||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page,
|
||||||
):
|
):
|
||||||
"""Test that build_ui sets up the drawer correctly."""
|
"""Test that build_ui sets up the drawer correctly."""
|
||||||
mock_tab_manager = Mock()
|
mock_tab_manager = Mock()
|
||||||
@@ -129,14 +129,14 @@ class TestOpenSettingsTab:
|
|||||||
# Get the settings content that was added
|
# Get the settings content that was added
|
||||||
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
|
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
|
||||||
|
|
||||||
# Find the save button and simulate click
|
# Find the save button - now nested in action_row container
|
||||||
save_btn = None
|
save_btn = None
|
||||||
for control in settings_content.controls:
|
for control in settings_content.controls:
|
||||||
if hasattr(control, "controls"):
|
if hasattr(control, "content") and hasattr(control.content, "controls"):
|
||||||
for sub_control in control.controls:
|
for sub_control in control.content.controls:
|
||||||
if (
|
if (
|
||||||
hasattr(sub_control, "text")
|
hasattr(sub_control, "text")
|
||||||
and sub_control.text == "Save Config"
|
and sub_control.text == "Save Configuration"
|
||||||
):
|
):
|
||||||
save_btn = sub_control
|
save_btn = sub_control
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user