Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 926b3a198d | |||
| 8db441612f | |||
| b34b8f23ff | |||
| 13ad0bcef6 | |||
| 64b9ac3df4 | |||
| ee521a9f60 | |||
| fd4e0c8a14 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -92,26 +92,26 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Download Linux artifact
|
||||
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3d2d08f
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: ren-browser-linux
|
||||
path: ./artifacts/linux
|
||||
|
||||
- name: Download APK artifact
|
||||
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3d2d08f
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: ren-browser-apk
|
||||
path: ./artifacts/apk
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
if: github.ref_type == 'tag'
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
./artifacts/linux/**/*
|
||||
./artifacts/apk/**/*
|
||||
./artifacts/linux/*
|
||||
./artifacts/apk/*
|
||||
name: Release ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
body: |
|
||||
## Release ${{ github.ref_name }}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ren-browser"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "A browser for the Reticulum Network."
|
||||
authors = [
|
||||
{name = "Sudo-Ivan"}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
This module provides services for listening to and collecting network
|
||||
announces from the Reticulum network.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import RNS
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Announce:
|
||||
"""Represents a Reticulum network announce.
|
||||
@@ -21,6 +21,7 @@ class Announce:
|
||||
display_name: str | None
|
||||
timestamp: int
|
||||
|
||||
|
||||
class AnnounceService:
|
||||
"""Service to listen for Reticulum announces and collect them.
|
||||
|
||||
@@ -60,7 +61,11 @@ class AnnounceService:
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
announce = Announce(destination_hash.hex(), display_name, ts)
|
||||
self.announces = [ann for ann in self.announces if ann.destination_hash != announce.destination_hash]
|
||||
self.announces = [
|
||||
ann
|
||||
for ann in self.announces
|
||||
if ann.destination_hash != announce.destination_hash
|
||||
]
|
||||
self.announces.insert(0, announce)
|
||||
if self.update_callback:
|
||||
self.update_callback(self.announces)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This module provides the entry point and platform-specific launchers for the
|
||||
Ren Browser, a browser for the Reticulum Network built with Flet.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import flet as ft
|
||||
@@ -15,36 +16,58 @@ from ren_browser.ui.ui import build_ui
|
||||
RENDERER = "plaintext"
|
||||
RNS_CONFIG_DIR = None
|
||||
|
||||
|
||||
async def main(page: Page):
|
||||
"""Initialize and launch the Ren Browser application.
|
||||
|
||||
Sets up the loading screen, initializes Reticulum network,
|
||||
and builds the main UI.
|
||||
"""
|
||||
page.title = "Ren Browser"
|
||||
page.theme_mode = ft.ThemeMode.DARK
|
||||
|
||||
loader = ft.Container(
|
||||
expand=True,
|
||||
alignment=ft.alignment.center,
|
||||
bgcolor=ft.Colors.SURFACE,
|
||||
content=ft.Column(
|
||||
[ft.ProgressRing(), ft.Text("Initializing reticulum network")],
|
||||
[
|
||||
ft.ProgressRing(color=ft.Colors.PRIMARY, width=50, height=50),
|
||||
ft.Container(height=20),
|
||||
ft.Text(
|
||||
"Initializing Reticulum Network...",
|
||||
size=16,
|
||||
color=ft.Colors.ON_SURFACE,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=10,
|
||||
),
|
||||
)
|
||||
page.add(loader)
|
||||
page.update()
|
||||
|
||||
def init_ret():
|
||||
import time
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Initialize storage system
|
||||
storage = initialize_storage(page)
|
||||
|
||||
# Get Reticulum config directory
|
||||
if RNS_CONFIG_DIR:
|
||||
config_dir = RNS_CONFIG_DIR
|
||||
else:
|
||||
# Get Reticulum config directory from storage manager
|
||||
config_dir = storage.get_reticulum_config_path()
|
||||
|
||||
# Update the global RNS_CONFIG_DIR so RNS uses the right path
|
||||
global RNS_CONFIG_DIR
|
||||
RNS_CONFIG_DIR = str(config_dir)
|
||||
|
||||
try:
|
||||
# Set up logging capture first, before RNS init
|
||||
import ren_browser.logs
|
||||
|
||||
ren_browser.logs.setup_rns_logging()
|
||||
RNS.Reticulum(str(config_dir))
|
||||
except (OSError, ValueError):
|
||||
@@ -55,14 +78,31 @@ async def main(page: Page):
|
||||
|
||||
page.run_thread(init_ret)
|
||||
|
||||
|
||||
def run():
|
||||
"""Run Ren Browser with command line argument parsing."""
|
||||
global RENDERER, RNS_CONFIG_DIR
|
||||
parser = argparse.ArgumentParser(description="Ren Browser")
|
||||
parser.add_argument("-r", "--renderer", choices=["plaintext", "micron"], default=RENDERER, help="Select renderer (plaintext or micron)")
|
||||
parser.add_argument("-w", "--web", action="store_true", help="Launch in web browser mode")
|
||||
parser.add_argument("-p", "--port", type=int, default=None, help="Port for web server")
|
||||
parser.add_argument("-c", "--config-dir", type=str, default=None, help="RNS config directory (default: ~/.reticulum/)")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--renderer",
|
||||
choices=["plaintext", "micron"],
|
||||
default=RENDERER,
|
||||
help="Select renderer (plaintext or micron)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w", "--web", action="store_true", help="Launch in web browser mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--port", type=int, default=None, help="Port for web server"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="RNS config directory (default: ~/.reticulum/)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
RENDERER = args.renderer
|
||||
|
||||
@@ -71,6 +111,7 @@ def run():
|
||||
RNS_CONFIG_DIR = args.config_dir
|
||||
else:
|
||||
import pathlib
|
||||
|
||||
RNS_CONFIG_DIR = str(pathlib.Path.home() / ".reticulum")
|
||||
|
||||
if args.web:
|
||||
@@ -81,33 +122,41 @@ def run():
|
||||
else:
|
||||
ft.app(main)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
|
||||
def web():
|
||||
"""Launch Ren Browser in web mode."""
|
||||
ft.app(main, view=AppView.WEB_BROWSER)
|
||||
|
||||
|
||||
def android():
|
||||
"""Launch Ren Browser in Android mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def ios():
|
||||
"""Launch Ren Browser in iOS mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def run_dev():
|
||||
"""Launch Ren Browser in desktop mode."""
|
||||
ft.app(main)
|
||||
|
||||
|
||||
def web_dev():
|
||||
"""Launch Ren Browser in web mode."""
|
||||
ft.app(main, view=AppView.WEB_BROWSER)
|
||||
|
||||
|
||||
def android_dev():
|
||||
"""Launch Ren Browser in Android mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def ios_dev():
|
||||
"""Launch Ren Browser in iOS mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides keyboard event handling and delegation to tab manager
|
||||
and UI components.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides centralized logging for application events, errors, and
|
||||
Reticulum network activities.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import RNS
|
||||
@@ -12,6 +13,7 @@ ERROR_LOGS: list[str] = []
|
||||
RET_LOGS: list[str] = []
|
||||
_original_rns_log = RNS.log
|
||||
|
||||
|
||||
def log_ret(msg, *args, **kwargs):
|
||||
"""Log Reticulum messages with timestamp.
|
||||
|
||||
@@ -25,14 +27,16 @@ def log_ret(msg, *args, **kwargs):
|
||||
RET_LOGS.append(f"[{timestamp}] {msg}")
|
||||
return _original_rns_log(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def setup_rns_logging():
|
||||
"""Set up RNS log replacement. Call this after RNS.Reticulum initialization."""
|
||||
global _original_rns_log
|
||||
# Only set up if not already done and if RNS.log is not already our function
|
||||
if RNS.log != log_ret and _original_rns_log != log_ret:
|
||||
if RNS.log is not log_ret and _original_rns_log is not log_ret:
|
||||
_original_rns_log = RNS.log
|
||||
RNS.log = log_ret
|
||||
|
||||
|
||||
def log_error(msg: str):
|
||||
"""Log error messages to both error and application logs.
|
||||
|
||||
@@ -44,6 +48,7 @@ def log_error(msg: str):
|
||||
ERROR_LOGS.append(f"[{timestamp}] {msg}")
|
||||
APP_LOGS.append(f"[{timestamp}] ERROR: {msg}")
|
||||
|
||||
|
||||
def log_app(msg: str):
|
||||
"""Log application messages.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Handles downloading pages from the Reticulum network using
|
||||
the nomadnetwork protocol.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -10,7 +11,6 @@ from dataclasses import dataclass
|
||||
import RNS
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageRequest:
|
||||
"""Represents a request for a page from the Reticulum network.
|
||||
@@ -22,6 +22,7 @@ class PageRequest:
|
||||
page_path: str
|
||||
field_data: dict | None = None
|
||||
|
||||
|
||||
class PageFetcher:
|
||||
"""Fetcher to download pages from the Reticulum network."""
|
||||
|
||||
@@ -43,7 +44,9 @@ class PageFetcher:
|
||||
Exception: If no path to destination or identity not found.
|
||||
|
||||
"""
|
||||
RNS.log(f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}")
|
||||
RNS.log(
|
||||
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}"
|
||||
)
|
||||
dest_bytes = bytes.fromhex(req.destination_hash)
|
||||
if not RNS.Transport.has_path(dest_bytes):
|
||||
RNS.Transport.request_path(dest_bytes)
|
||||
@@ -79,9 +82,16 @@ class PageFetcher:
|
||||
ev.set()
|
||||
|
||||
link.set_link_established_callback(
|
||||
lambda link: link.request(req.page_path, req.field_data, response_callback=on_response, failed_callback=on_failed)
|
||||
lambda link: link.request(
|
||||
req.page_path,
|
||||
req.field_data,
|
||||
response_callback=on_response,
|
||||
failed_callback=on_failed,
|
||||
)
|
||||
)
|
||||
ev.wait(timeout=15)
|
||||
data_str = result["data"] or "No content received"
|
||||
RNS.log(f"PageFetcher: received data for {req.destination_hash}:{req.page_path}")
|
||||
RNS.log(
|
||||
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}"
|
||||
)
|
||||
return data_str
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides rendering capabilities for micron markup content,
|
||||
currently implemented as a placeholder.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Provides fallback rendering for plaintext content and source viewing.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides persistent storage for configuration, bookmarks, history,
|
||||
and other application data across different platforms.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
@@ -38,7 +39,9 @@ class StorageManager:
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
# Android - use app's private files directory
|
||||
storage_dir = pathlib.Path("/data/data/com.ren_browser/files")
|
||||
elif hasattr(os, "uname") and "iOS" in str(getattr(os, "uname", lambda: "")()).replace("iPhone", "iOS"):
|
||||
elif hasattr(os, "uname") and "iOS" in str(
|
||||
getattr(os, "uname", lambda: "")()
|
||||
).replace("iPhone", "iOS"):
|
||||
# iOS - use app's documents directory
|
||||
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
|
||||
else:
|
||||
@@ -46,7 +49,9 @@ class StorageManager:
|
||||
if "APPDATA" in os.environ: # Windows
|
||||
storage_dir = pathlib.Path(os.environ["APPDATA"]) / "ren_browser"
|
||||
elif "XDG_CONFIG_HOME" in os.environ: # Linux XDG standard
|
||||
storage_dir = pathlib.Path(os.environ["XDG_CONFIG_HOME"]) / "ren_browser"
|
||||
storage_dir = (
|
||||
pathlib.Path(os.environ["XDG_CONFIG_HOME"]) / "ren_browser"
|
||||
)
|
||||
else:
|
||||
storage_dir = pathlib.Path.home() / ".ren_browser"
|
||||
|
||||
@@ -58,6 +63,7 @@ class StorageManager:
|
||||
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
except (OSError, PermissionError):
|
||||
import tempfile
|
||||
|
||||
self._storage_dir = pathlib.Path(tempfile.gettempdir()) / "ren_browser"
|
||||
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -65,17 +71,21 @@ class StorageManager:
|
||||
"""Get the path to the main configuration file."""
|
||||
return self._storage_dir / "config"
|
||||
|
||||
@staticmethod
|
||||
def get_reticulum_config_path() -> pathlib.Path:
|
||||
def get_reticulum_config_path(self) -> pathlib.Path:
|
||||
"""Get the path to the Reticulum configuration directory."""
|
||||
# Check for global override from app
|
||||
try:
|
||||
from ren_browser.app import RNS_CONFIG_DIR
|
||||
|
||||
if RNS_CONFIG_DIR:
|
||||
return pathlib.Path(RNS_CONFIG_DIR)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# On Android, use app storage directory instead of ~/.reticulum
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
return self._storage_dir / "reticulum"
|
||||
|
||||
# Default to standard RNS config directory
|
||||
return pathlib.Path.home() / ".reticulum"
|
||||
|
||||
@@ -90,6 +100,7 @@ class StorageManager:
|
||||
|
||||
"""
|
||||
try:
|
||||
# Always save to client storage first (most reliable on mobile)
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
self.page.client_storage.set("ren_browser_config", config_content)
|
||||
|
||||
@@ -100,6 +111,7 @@ class StorageManager:
|
||||
|
||||
# Also save to local config path as backup
|
||||
config_path = self.get_config_path()
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(config_content, encoding="utf-8")
|
||||
return True
|
||||
|
||||
@@ -111,7 +123,9 @@ class StorageManager:
|
||||
try:
|
||||
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_error", f"File save failed: {error}")
|
||||
self.page.client_storage.set(
|
||||
"ren_browser_config_error", f"File save failed: {error}"
|
||||
)
|
||||
return True
|
||||
|
||||
try:
|
||||
@@ -122,6 +136,7 @@ class StorageManager:
|
||||
pass
|
||||
|
||||
import tempfile
|
||||
|
||||
temp_path = pathlib.Path(tempfile.gettempdir()) / "ren_browser_config.txt"
|
||||
temp_path.write_text(config_content, encoding="utf-8")
|
||||
return True
|
||||
@@ -136,6 +151,13 @@ class StorageManager:
|
||||
Configuration text, or empty string if not found
|
||||
|
||||
"""
|
||||
# On Android, prioritize client storage first as it's more reliable
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
try:
|
||||
reticulum_config_path = self.get_reticulum_config_path() / "config"
|
||||
if reticulum_config_path.exists():
|
||||
@@ -145,13 +167,18 @@ class StorageManager:
|
||||
if config_path.exists():
|
||||
return config_path.read_text(encoding="utf-8")
|
||||
|
||||
# Fallback to client storage for non-Android or if files don't exist
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
except (OSError, PermissionError, UnicodeDecodeError):
|
||||
pass
|
||||
# If file access fails, try client storage as fallback
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
return ""
|
||||
|
||||
@@ -163,7 +190,9 @@ class StorageManager:
|
||||
json.dump(bookmarks, f, indent=2)
|
||||
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
self.page.client_storage.set("ren_browser_bookmarks", json.dumps(bookmarks))
|
||||
self.page.client_storage.set(
|
||||
"ren_browser_bookmarks", json.dumps(bookmarks)
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
@@ -267,6 +296,7 @@ def get_rns_config_directory() -> str:
|
||||
"""Get the RNS config directory, checking for global override."""
|
||||
try:
|
||||
from ren_browser.app import RNS_CONFIG_DIR
|
||||
|
||||
if RNS_CONFIG_DIR:
|
||||
return RNS_CONFIG_DIR
|
||||
except ImportError:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides tab creation, switching, and content management functionality
|
||||
for the browser interface.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import flet as ft
|
||||
@@ -25,15 +26,26 @@ class TabsManager:
|
||||
|
||||
"""
|
||||
import ren_browser.app as app_module
|
||||
|
||||
self.page = page
|
||||
self.manager = SimpleNamespace(tabs=[], index=0)
|
||||
self.tab_bar = ft.Row(spacing=4)
|
||||
self.content_container = ft.Container(expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5))
|
||||
self.content_container = ft.Container(
|
||||
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5)
|
||||
)
|
||||
|
||||
default_content = render_micron("Welcome to Ren Browser") if app_module.RENDERER == "micron" else render_plaintext("Welcome to Ren Browser")
|
||||
default_content = (
|
||||
render_micron("Welcome to Ren Browser")
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext("Welcome to Ren Browser")
|
||||
)
|
||||
self._add_tab_internal("Home", default_content)
|
||||
self.add_btn = ft.IconButton(ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click)
|
||||
self.close_btn = ft.IconButton(ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click)
|
||||
self.add_btn = ft.IconButton(
|
||||
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click
|
||||
)
|
||||
self.close_btn = ft.IconButton(
|
||||
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click
|
||||
)
|
||||
self.tab_bar.controls.extend([self.add_btn, self.close_btn])
|
||||
self.select_tab(0)
|
||||
|
||||
@@ -43,9 +55,13 @@ class TabsManager:
|
||||
value=title,
|
||||
expand=True,
|
||||
text_style=ft.TextStyle(size=12),
|
||||
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8)
|
||||
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8),
|
||||
)
|
||||
go_btn = ft.IconButton(
|
||||
ft.Icons.OPEN_IN_BROWSER,
|
||||
tooltip="Load URL",
|
||||
on_click=lambda e, i=idx: self._on_tab_go(e, i),
|
||||
)
|
||||
go_btn = ft.IconButton(ft.Icons.OPEN_IN_BROWSER, tooltip="Load URL", on_click=lambda e, i=idx: self._on_tab_go(e, i))
|
||||
content_control = content
|
||||
tab_content = ft.Column(
|
||||
expand=True,
|
||||
@@ -53,13 +69,15 @@ class TabsManager:
|
||||
content_control,
|
||||
],
|
||||
)
|
||||
self.manager.tabs.append({
|
||||
self.manager.tabs.append(
|
||||
{
|
||||
"title": title,
|
||||
"url_field": url_field,
|
||||
"go_btn": go_btn,
|
||||
"content_control": content_control,
|
||||
"content": tab_content,
|
||||
})
|
||||
}
|
||||
)
|
||||
btn = ft.Container(
|
||||
content=ft.Text(title),
|
||||
on_click=lambda e, i=idx: self.select_tab(i),
|
||||
@@ -74,7 +92,12 @@ class TabsManager:
|
||||
title = f"Tab {len(self.manager.tabs) + 1}"
|
||||
content_text = f"Content for {title}"
|
||||
import ren_browser.app as app_module
|
||||
content = render_micron(content_text) if app_module.RENDERER == "micron" else render_plaintext(content_text)
|
||||
|
||||
content = (
|
||||
render_micron(content_text)
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext(content_text)
|
||||
)
|
||||
self._add_tab_internal(title, content)
|
||||
self.select_tab(len(self.manager.tabs) - 1)
|
||||
self.page.update()
|
||||
@@ -114,7 +137,12 @@ class TabsManager:
|
||||
return
|
||||
placeholder_text = f"Loading content for {url}"
|
||||
import ren_browser.app as app_module
|
||||
new_control = render_micron(placeholder_text) if app_module.RENDERER == "micron" else render_plaintext(placeholder_text)
|
||||
|
||||
new_control = (
|
||||
render_micron(placeholder_text)
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext(placeholder_text)
|
||||
)
|
||||
tab["content_control"] = new_control
|
||||
tab["content"].controls[0] = new_control
|
||||
if self.manager.index == idx:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides configuration management, log viewing, and storage
|
||||
information display.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from ren_browser.logs import ERROR_LOGS, RET_LOGS
|
||||
@@ -35,12 +36,20 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
try:
|
||||
success = storage.save_config(config_field.value)
|
||||
if success:
|
||||
page.snack_bar = ft.SnackBar(ft.Text("Config saved successfully. Please restart the app."), open=True)
|
||||
page.snack_bar = ft.SnackBar(
|
||||
ft.Text("Config saved successfully. Please restart the app."),
|
||||
open=True,
|
||||
)
|
||||
else:
|
||||
page.snack_bar = ft.SnackBar(ft.Text("Error saving config: Storage operation failed"), open=True)
|
||||
page.snack_bar = ft.SnackBar(
|
||||
ft.Text("Error saving config: Storage operation failed"), open=True
|
||||
)
|
||||
except Exception as ex:
|
||||
page.snack_bar = ft.SnackBar(ft.Text(f"Error saving config: {ex}"), open=True)
|
||||
page.snack_bar = ft.SnackBar(
|
||||
ft.Text(f"Error saving config: {ex}"), open=True
|
||||
)
|
||||
page.update()
|
||||
|
||||
save_btn = ft.ElevatedButton("Save and Restart", on_click=on_save_config)
|
||||
error_field = ft.TextField(
|
||||
label="Error Logs",
|
||||
@@ -69,22 +78,29 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
)
|
||||
|
||||
content_placeholder = ft.Container(expand=True)
|
||||
|
||||
def show_config(ev):
|
||||
content_placeholder.content = config_field
|
||||
page.update()
|
||||
|
||||
def show_errors(ev):
|
||||
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged."
|
||||
content_placeholder.content = error_field
|
||||
page.update()
|
||||
|
||||
def show_ret_logs(ev):
|
||||
ret_field.value = "\n".join(RET_LOGS) or "No Reticulum logs."
|
||||
content_placeholder.content = ret_field
|
||||
page.update()
|
||||
|
||||
def show_storage_info(ev):
|
||||
storage_info = storage.get_storage_info()
|
||||
storage_field.value = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
|
||||
storage_field.value = "\n".join(
|
||||
[f"{key}: {value}" for key, value in storage_info.items()]
|
||||
)
|
||||
content_placeholder.content = storage_field
|
||||
page.update()
|
||||
|
||||
def refresh_current_view(ev):
|
||||
# Refresh the currently displayed content
|
||||
if content_placeholder.content == error_field:
|
||||
@@ -95,12 +111,15 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
show_storage_info(ev)
|
||||
elif content_placeholder.content == config_field:
|
||||
show_config(ev)
|
||||
|
||||
btn_config = ft.ElevatedButton("Config", on_click=show_config)
|
||||
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors)
|
||||
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs)
|
||||
btn_storage = ft.ElevatedButton("Storage", on_click=show_storage_info)
|
||||
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])
|
||||
button_row = ft.Row(
|
||||
controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh]
|
||||
)
|
||||
content_placeholder.content = config_field
|
||||
settings_content = ft.Column(
|
||||
expand=True,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Builds the complete browser interface including tabs, navigation,
|
||||
announce handling, and content rendering.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
from flet import Page
|
||||
|
||||
@@ -27,10 +28,12 @@ def build_ui(page: Page):
|
||||
|
||||
page_fetcher = PageFetcher()
|
||||
announce_list = ft.ListView(expand=True, spacing=1)
|
||||
|
||||
def update_announces(ann_list):
|
||||
announce_list.controls.clear()
|
||||
for ann in ann_list:
|
||||
label = ann.display_name or ann.destination_hash
|
||||
|
||||
def on_click_ann(e, dest=ann.destination_hash, disp=ann.display_name):
|
||||
title = disp or "Anonymous"
|
||||
full_url = f"{dest}:/page/index.mu"
|
||||
@@ -41,12 +44,14 @@ def build_ui(page: Page):
|
||||
tab["url_field"].value = full_url
|
||||
tab_manager.select_tab(idx)
|
||||
page.update()
|
||||
|
||||
def fetch_and_update():
|
||||
req = PageRequest(destination_hash=dest, page_path="/page/index.mu")
|
||||
try:
|
||||
result = page_fetcher.fetch_page(req)
|
||||
except Exception as ex:
|
||||
import ren_browser.app as app_module
|
||||
|
||||
app_module.log_error(str(ex))
|
||||
result = f"Error: {ex}"
|
||||
try:
|
||||
@@ -62,13 +67,21 @@ def build_ui(page: Page):
|
||||
if tab_manager.manager.index == idx:
|
||||
tab_manager.content_container.content = tab["content"]
|
||||
page.update()
|
||||
|
||||
page.run_thread(fetch_and_update)
|
||||
|
||||
announce_list.controls.append(ft.TextButton(label, on_click=on_click_ann))
|
||||
page.update()
|
||||
|
||||
AnnounceService(update_callback=update_announces)
|
||||
page.drawer = ft.NavigationDrawer(
|
||||
controls=[
|
||||
ft.Text("Announcements", weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER, expand=True),
|
||||
ft.Text(
|
||||
"Announcements",
|
||||
weight=ft.FontWeight.BOLD,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
expand=True,
|
||||
),
|
||||
ft.Divider(),
|
||||
announce_list,
|
||||
],
|
||||
@@ -76,12 +89,22 @@ def build_ui(page: Page):
|
||||
page.appbar.leading = ft.IconButton(
|
||||
ft.Icons.MENU,
|
||||
tooltip="Toggle sidebar",
|
||||
on_click=lambda e: (setattr(page.drawer, "open", not page.drawer.open), page.update()),
|
||||
on_click=lambda e: (
|
||||
setattr(page.drawer, "open", not page.drawer.open),
|
||||
page.update(),
|
||||
),
|
||||
)
|
||||
|
||||
tab_manager = TabsManager(page)
|
||||
from ren_browser.ui.settings import open_settings_tab
|
||||
page.appbar.actions = [ft.IconButton(ft.Icons.SETTINGS, tooltip="Settings", on_click=lambda e: open_settings_tab(page, tab_manager))]
|
||||
|
||||
page.appbar.actions = [
|
||||
ft.IconButton(
|
||||
ft.Icons.SETTINGS,
|
||||
tooltip="Settings",
|
||||
on_click=lambda e: open_settings_tab(page, tab_manager),
|
||||
)
|
||||
]
|
||||
Shortcuts(page, tab_manager)
|
||||
url_bar = ft.Row(
|
||||
controls=[
|
||||
@@ -91,15 +114,19 @@ def build_ui(page: Page):
|
||||
)
|
||||
page.appbar.title = url_bar
|
||||
orig_select_tab = tab_manager.select_tab
|
||||
|
||||
def _select_tab_and_update_url(i):
|
||||
orig_select_tab(i)
|
||||
tab = tab_manager.manager.tabs[i]
|
||||
url_bar.controls.clear()
|
||||
url_bar.controls.extend([tab["url_field"], tab["go_btn"]])
|
||||
page.update()
|
||||
|
||||
tab_manager.select_tab = _select_tab_and_update_url
|
||||
|
||||
def _update_content_width(e=None):
|
||||
tab_manager.content_container.width = page.width
|
||||
|
||||
_update_content_width()
|
||||
page.on_resized = lambda e: (_update_content_width(), page.update())
|
||||
main_area = ft.Column(
|
||||
|
||||
@@ -36,6 +36,7 @@ def mock_rns():
|
||||
|
||||
# Mock at the module level for all imports
|
||||
import sys
|
||||
|
||||
sys.modules["RNS"] = mock_rns
|
||||
|
||||
yield mock_rns
|
||||
@@ -51,7 +52,7 @@ def sample_announce_data():
|
||||
return {
|
||||
"destination_hash": "1234567890abcdef",
|
||||
"display_name": "Test Node",
|
||||
"timestamp": 1234567890
|
||||
"timestamp": 1234567890,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +60,9 @@ def sample_announce_data():
|
||||
def sample_page_request():
|
||||
"""Sample page request for testing."""
|
||||
from ren_browser.pages.page_request import 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
|
||||
)
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ def mock_storage_manager():
|
||||
mock_storage.get_config_path.return_value = Mock()
|
||||
mock_storage.get_reticulum_config_path.return_value = Mock()
|
||||
mock_storage.get_storage_info.return_value = {
|
||||
'storage_dir': '/mock/storage',
|
||||
'config_path': '/mock/storage/config.txt',
|
||||
'reticulum_config_path': '/mock/storage/reticulum',
|
||||
'storage_dir_exists': True,
|
||||
'storage_dir_writable': True,
|
||||
'has_client_storage': True,
|
||||
"storage_dir": "/mock/storage",
|
||||
"config_path": "/mock/storage/config.txt",
|
||||
"reticulum_config_path": "/mock/storage/reticulum",
|
||||
"storage_dir_exists": True,
|
||||
"storage_dir_writable": True,
|
||||
"has_client_storage": True,
|
||||
}
|
||||
return mock_storage
|
||||
|
||||
@@ -28,8 +28,14 @@ class TestAppIntegration:
|
||||
def test_entry_points_exist(self):
|
||||
"""Test that all expected entry points exist and are callable."""
|
||||
entry_points = [
|
||||
"run", "web", "android", "ios",
|
||||
"run_dev", "web_dev", "android_dev", "ios_dev"
|
||||
"run",
|
||||
"web",
|
||||
"android",
|
||||
"ios",
|
||||
"run_dev",
|
||||
"web_dev",
|
||||
"android_dev",
|
||||
"ios_dev",
|
||||
]
|
||||
|
||||
for entry_point in entry_points:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from ren_browser.announces.announces import Announce
|
||||
|
||||
|
||||
@@ -10,7 +9,7 @@ class TestAnnounce:
|
||||
announce = Announce(
|
||||
destination_hash="1234567890abcdef",
|
||||
display_name="Test Node",
|
||||
timestamp=1234567890
|
||||
timestamp=1234567890,
|
||||
)
|
||||
|
||||
assert announce.destination_hash == "1234567890abcdef"
|
||||
@@ -20,15 +19,14 @@ class TestAnnounce:
|
||||
def test_announce_with_none_display_name(self):
|
||||
"""Test Announce creation with None display name."""
|
||||
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.display_name is None
|
||||
assert announce.timestamp == 1234567890
|
||||
|
||||
|
||||
class TestAnnounceService:
|
||||
"""Test cases for the AnnounceService class.
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ class TestApp:
|
||||
|
||||
def test_run_with_default_args(self, mock_rns):
|
||||
"""Test run function with default arguments."""
|
||||
with patch("sys.argv", ["ren-browser"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with patch("sys.argv", ["ren-browser"]), patch("flet.app") as mock_ft_app:
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -46,9 +44,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_web_flag(self, mock_rns):
|
||||
"""Test run function with web flag."""
|
||||
with patch("sys.argv", ["ren-browser", "--web"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--web"]),
|
||||
patch("flet.app") as mock_ft_app,
|
||||
):
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -58,9 +57,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_web_and_port(self, mock_rns):
|
||||
"""Test run function with web flag and custom port."""
|
||||
with patch("sys.argv", ["ren-browser", "--web", "--port", "8080"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--web", "--port", "8080"]),
|
||||
patch("flet.app") as mock_ft_app,
|
||||
):
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -71,9 +71,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_renderer_flag(self, mock_rns):
|
||||
"""Test run function with renderer selection."""
|
||||
with patch("sys.argv", ["ren-browser", "--renderer", "micron"]), \
|
||||
patch("flet.app"):
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--renderer", "micron"]),
|
||||
patch("flet.app"),
|
||||
):
|
||||
app.run()
|
||||
|
||||
assert app.RENDERER == "micron"
|
||||
@@ -131,8 +132,10 @@ class TestApp:
|
||||
"""Test that RENDERER global is properly updated."""
|
||||
original_renderer = app.RENDERER
|
||||
|
||||
with patch("sys.argv", ["ren-browser", "--renderer", "micron"]), \
|
||||
patch("flet.app"):
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--renderer", "micron"]),
|
||||
patch("flet.app"),
|
||||
):
|
||||
app.run()
|
||||
assert app.RENDERER == "micron"
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ class TestLogsModule:
|
||||
|
||||
assert len(logs.RET_LOGS) == 1
|
||||
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
|
||||
logs._original_rns_log.assert_called_once_with("Test RNS message", "arg1", kwarg1="value1")
|
||||
logs._original_rns_log.assert_called_once_with(
|
||||
"Test RNS message", "arg1", kwarg1="value1"
|
||||
)
|
||||
assert result == "original_result"
|
||||
|
||||
def test_multiple_log_calls(self):
|
||||
|
||||
@@ -7,8 +7,7 @@ class TestPageRequest:
|
||||
def test_page_request_creation(self):
|
||||
"""Test basic PageRequest creation."""
|
||||
request = PageRequest(
|
||||
destination_hash="1234567890abcdef",
|
||||
page_path="/page/index.mu"
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu"
|
||||
)
|
||||
|
||||
assert request.destination_hash == "1234567890abcdef"
|
||||
@@ -21,7 +20,7 @@ class TestPageRequest:
|
||||
request = PageRequest(
|
||||
destination_hash="1234567890abcdef",
|
||||
page_path="/page/form.mu",
|
||||
field_data=field_data
|
||||
field_data=field_data,
|
||||
)
|
||||
|
||||
assert request.destination_hash == "1234567890abcdef"
|
||||
@@ -59,7 +58,7 @@ class TestPageFetcher:
|
||||
requests = [
|
||||
PageRequest("hash1", "/index.mu"),
|
||||
PageRequest("hash2", "/about.mu", {"form": "data"}),
|
||||
PageRequest("hash3", "/contact.mu")
|
||||
PageRequest("hash3", "/contact.mu"),
|
||||
]
|
||||
|
||||
# Test that requests have the expected structure
|
||||
|
||||
@@ -215,7 +215,7 @@ class TestShortcuts:
|
||||
url_field2 = Mock()
|
||||
mock_tab_manager.manager.tabs = [
|
||||
{"url_field": url_field1},
|
||||
{"url_field": url_field2}
|
||||
{"url_field": url_field2},
|
||||
]
|
||||
mock_tab_manager.manager.index = 1 # Second tab
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ren_browser.storage.storage import StorageManager, get_storage_manager, initialize_storage
|
||||
from ren_browser.storage.storage import (
|
||||
StorageManager,
|
||||
get_storage_manager,
|
||||
initialize_storage,
|
||||
)
|
||||
|
||||
|
||||
class TestStorageManager:
|
||||
@@ -13,11 +17,13 @@ class TestStorageManager:
|
||||
|
||||
def test_storage_manager_init_without_page(self):
|
||||
"""Test StorageManager initialization without a page."""
|
||||
with patch('ren_browser.storage.storage.StorageManager._get_storage_directory') as mock_get_dir:
|
||||
mock_dir = Path('/mock/storage')
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
|
||||
with patch('pathlib.Path.mkdir') as mock_mkdir:
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir:
|
||||
storage = StorageManager()
|
||||
|
||||
assert storage.page is None
|
||||
@@ -28,11 +34,13 @@ class TestStorageManager:
|
||||
"""Test StorageManager initialization with a page."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._get_storage_directory') as mock_get_dir:
|
||||
mock_dir = Path('/mock/storage')
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
|
||||
with patch('pathlib.Path.mkdir'):
|
||||
with patch("pathlib.Path.mkdir"):
|
||||
storage = StorageManager(mock_page)
|
||||
|
||||
assert storage.page == mock_page
|
||||
@@ -40,14 +48,19 @@ class TestStorageManager:
|
||||
|
||||
def test_get_storage_directory_desktop(self):
|
||||
"""Test storage directory detection for desktop platforms."""
|
||||
with patch('os.name', 'posix'), \
|
||||
patch.dict('os.environ', {'XDG_CONFIG_HOME': '/home/user/.config'}, clear=True), \
|
||||
patch('pathlib.Path.mkdir'):
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._ensure_storage_directory'):
|
||||
with (
|
||||
patch("os.name", "posix"),
|
||||
patch.dict(
|
||||
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
|
||||
),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
expected_dir = Path('/home/user/.config') / 'ren_browser'
|
||||
expected_dir = Path("/home/user/.config") / "ren_browser"
|
||||
assert storage._storage_dir == expected_dir
|
||||
|
||||
def test_get_storage_directory_windows(self):
|
||||
@@ -57,14 +70,17 @@ class TestStorageManager:
|
||||
|
||||
def test_get_storage_directory_android(self):
|
||||
"""Test storage directory detection for Android."""
|
||||
with patch('os.name', 'posix'), \
|
||||
patch.dict('os.environ', {'ANDROID_ROOT': '/system'}, clear=True), \
|
||||
patch('pathlib.Path.mkdir'):
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._ensure_storage_directory'):
|
||||
with (
|
||||
patch("os.name", "posix"),
|
||||
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
expected_dir = Path('/data/data/com.ren_browser/files')
|
||||
expected_dir = Path("/data/data/com.ren_browser/files")
|
||||
assert storage._storage_dir == expected_dir
|
||||
|
||||
def test_get_config_path(self):
|
||||
@@ -74,7 +90,7 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
config_path = storage.get_config_path()
|
||||
expected_path = Path(temp_dir) / 'config'
|
||||
expected_path = Path(temp_dir) / "config"
|
||||
assert config_path == expected_path
|
||||
|
||||
def test_get_reticulum_config_path(self):
|
||||
@@ -82,7 +98,7 @@ class TestStorageManager:
|
||||
storage = StorageManager()
|
||||
|
||||
config_path = storage.get_reticulum_config_path()
|
||||
expected_path = Path.home() / '.reticulum'
|
||||
expected_path = Path.home() / ".reticulum"
|
||||
assert config_path == expected_path
|
||||
|
||||
def test_save_config_success(self):
|
||||
@@ -92,14 +108,18 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
assert result is True
|
||||
config_path = storage.get_config_path()
|
||||
assert config_path.exists()
|
||||
assert config_path.read_text(encoding='utf-8') == config_content
|
||||
assert config_path.read_text(encoding="utf-8") == config_content
|
||||
|
||||
def test_save_config_with_client_storage(self):
|
||||
"""Test config saving with client storage."""
|
||||
@@ -111,12 +131,18 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
assert result is True
|
||||
mock_page.client_storage.set.assert_called_with('ren_browser_config', config_content)
|
||||
mock_page.client_storage.set.assert_called_with(
|
||||
"ren_browser_config", config_content
|
||||
)
|
||||
|
||||
def test_save_config_fallback(self):
|
||||
"""Test config saving fallback when file system fails."""
|
||||
@@ -128,14 +154,23 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to use temp dir and cause failure
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch('pathlib.Path.write_text', side_effect=PermissionError("Access denied")):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
with patch(
|
||||
"pathlib.Path.write_text",
|
||||
side_effect=PermissionError("Access denied"),
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
assert result is True
|
||||
# Check that the config was set to client storage
|
||||
mock_page.client_storage.set.assert_any_call('ren_browser_config', config_content)
|
||||
mock_page.client_storage.set.assert_any_call(
|
||||
"ren_browser_config", config_content
|
||||
)
|
||||
# Verify that client storage was called at least once
|
||||
assert mock_page.client_storage.set.call_count >= 1
|
||||
|
||||
@@ -146,10 +181,14 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
config_path = storage.get_config_path()
|
||||
config_path.write_text(config_content, encoding='utf-8')
|
||||
config_path.write_text(config_content, encoding="utf-8")
|
||||
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == config_content
|
||||
@@ -164,10 +203,14 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == "client storage config"
|
||||
mock_page.client_storage.get.assert_called_with('ren_browser_config')
|
||||
mock_page.client_storage.get.assert_called_with("ren_browser_config")
|
||||
|
||||
def test_load_config_default(self):
|
||||
"""Test loading default config when no config exists."""
|
||||
@@ -176,7 +219,11 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == ""
|
||||
|
||||
@@ -190,10 +237,10 @@ class TestStorageManager:
|
||||
result = storage.save_bookmarks(bookmarks)
|
||||
|
||||
assert result is True
|
||||
bookmarks_path = storage._storage_dir / 'bookmarks.json'
|
||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||
assert bookmarks_path.exists()
|
||||
|
||||
with open(bookmarks_path, 'r', encoding='utf-8') as f:
|
||||
with open(bookmarks_path, "r", encoding="utf-8") as f:
|
||||
loaded_bookmarks = json.load(f)
|
||||
assert loaded_bookmarks == bookmarks
|
||||
|
||||
@@ -204,9 +251,9 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
bookmarks = [{"name": "Test", "url": "test://example"}]
|
||||
bookmarks_path = storage._storage_dir / 'bookmarks.json'
|
||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||
|
||||
with open(bookmarks_path, 'w', encoding='utf-8') as f:
|
||||
with open(bookmarks_path, "w", encoding="utf-8") as f:
|
||||
json.dump(bookmarks, f)
|
||||
|
||||
loaded_bookmarks = storage.load_bookmarks()
|
||||
@@ -231,10 +278,10 @@ class TestStorageManager:
|
||||
result = storage.save_history(history)
|
||||
|
||||
assert result is True
|
||||
history_path = storage._storage_dir / 'history.json'
|
||||
history_path = storage._storage_dir / "history.json"
|
||||
assert history_path.exists()
|
||||
|
||||
with open(history_path, 'r', encoding='utf-8') as f:
|
||||
with open(history_path, "r", encoding="utf-8") as f:
|
||||
loaded_history = json.load(f)
|
||||
assert loaded_history == history
|
||||
|
||||
@@ -245,9 +292,9 @@ class TestStorageManager:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
history = [{"url": "test://example", "timestamp": 1234567890}]
|
||||
history_path = storage._storage_dir / 'history.json'
|
||||
history_path = storage._storage_dir / "history.json"
|
||||
|
||||
with open(history_path, 'w', encoding='utf-8') as f:
|
||||
with open(history_path, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f)
|
||||
|
||||
loaded_history = storage.load_history()
|
||||
@@ -264,27 +311,30 @@ class TestStorageManager:
|
||||
|
||||
info = storage.get_storage_info()
|
||||
|
||||
assert 'storage_dir' in info
|
||||
assert 'config_path' in info
|
||||
assert 'reticulum_config_path' in info
|
||||
assert 'storage_dir_exists' in info
|
||||
assert 'storage_dir_writable' in info
|
||||
assert 'has_client_storage' in info
|
||||
assert "storage_dir" in info
|
||||
assert "config_path" in info
|
||||
assert "reticulum_config_path" in info
|
||||
assert "storage_dir_exists" in info
|
||||
assert "storage_dir_writable" in info
|
||||
assert "has_client_storage" in info
|
||||
|
||||
assert info['storage_dir'] == str(Path(temp_dir))
|
||||
assert info['storage_dir_exists'] is True
|
||||
assert info['has_client_storage'] is True
|
||||
assert info["storage_dir"] == str(Path(temp_dir))
|
||||
assert info["storage_dir_exists"] is True
|
||||
assert info["has_client_storage"] is True
|
||||
|
||||
def test_storage_directory_fallback(self):
|
||||
"""Test fallback to temp directory when storage creation fails."""
|
||||
with patch.object(StorageManager, '_get_storage_directory') as mock_get_dir:
|
||||
mock_get_dir.return_value = Path('/nonexistent/path')
|
||||
with patch.object(StorageManager, "_get_storage_directory") as mock_get_dir:
|
||||
mock_get_dir.return_value = Path("/nonexistent/path")
|
||||
|
||||
with patch('pathlib.Path.mkdir', side_effect=[PermissionError("Access denied"), None]):
|
||||
with patch('tempfile.gettempdir', return_value='/tmp'):
|
||||
with patch(
|
||||
"pathlib.Path.mkdir",
|
||||
side_effect=[PermissionError("Access denied"), None],
|
||||
):
|
||||
with patch("tempfile.gettempdir", return_value="/tmp"):
|
||||
storage = StorageManager()
|
||||
|
||||
expected_fallback = Path('/tmp') / 'ren_browser'
|
||||
expected_fallback = Path("/tmp") / "ren_browser"
|
||||
assert storage._storage_dir == expected_fallback
|
||||
|
||||
|
||||
@@ -293,7 +343,7 @@ class TestStorageGlobalFunctions:
|
||||
|
||||
def test_get_storage_manager_singleton(self):
|
||||
"""Test that get_storage_manager returns the same instance."""
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage1 = get_storage_manager()
|
||||
storage2 = get_storage_manager()
|
||||
|
||||
@@ -303,7 +353,7 @@ class TestStorageGlobalFunctions:
|
||||
"""Test get_storage_manager with page parameter."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage = get_storage_manager(mock_page)
|
||||
|
||||
assert storage.page == mock_page
|
||||
@@ -312,7 +362,7 @@ class TestStorageGlobalFunctions:
|
||||
"""Test initialize_storage function."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage = initialize_storage(mock_page)
|
||||
|
||||
assert storage.page == mock_page
|
||||
@@ -329,9 +379,16 @@ class TestStorageManagerEdgeCases:
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
# Test with content that might cause encoding issues
|
||||
with patch('pathlib.Path.write_text', side_effect=UnicodeEncodeError('utf-8', '', 0, 1, 'error')):
|
||||
with patch(
|
||||
"pathlib.Path.write_text",
|
||||
side_effect=UnicodeEncodeError("utf-8", "", 0, 1, "error"),
|
||||
):
|
||||
result = storage.save_config("test content")
|
||||
# Should still succeed due to fallback
|
||||
assert result is False
|
||||
@@ -344,10 +401,14 @@ class TestStorageManagerEdgeCases:
|
||||
|
||||
# Create a config file with invalid encoding
|
||||
config_path = storage.get_config_path()
|
||||
config_path.write_bytes(b'\xff\xfe invalid utf-8')
|
||||
config_path.write_bytes(b"\xff\xfe invalid utf-8")
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
# Should return empty string when encoding fails
|
||||
config = storage.load_config()
|
||||
assert config == ""
|
||||
@@ -356,8 +417,10 @@ class TestStorageManagerEdgeCases:
|
||||
"""Test _is_writable when permission is denied."""
|
||||
storage = StorageManager()
|
||||
|
||||
with patch('pathlib.Path.write_text', side_effect=PermissionError("Access denied")):
|
||||
test_path = Path('/mock/path')
|
||||
with patch(
|
||||
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
|
||||
):
|
||||
test_path = Path("/mock/path")
|
||||
result = storage._is_writable(test_path)
|
||||
assert result is False
|
||||
|
||||
|
||||
@@ -13,17 +13,19 @@ class TestTabsManager:
|
||||
@pytest.fixture
|
||||
def tabs_manager(self, mock_page):
|
||||
"""Create a TabsManager instance for testing."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
return TabsManager(mock_page)
|
||||
|
||||
def test_tabs_manager_init(self, mock_page):
|
||||
"""Test TabsManager initialization."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
manager = TabsManager(mock_page)
|
||||
|
||||
@@ -55,9 +57,10 @@ class TestTabsManager:
|
||||
|
||||
def test_on_add_click(self, tabs_manager):
|
||||
"""Test adding a new tab via button click."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
initial_count = len(tabs_manager.manager.tabs)
|
||||
|
||||
@@ -220,7 +223,13 @@ class TestTabsManager:
|
||||
tabs_manager._add_tab_internal("Tab 3", content2)
|
||||
|
||||
tabs_manager.select_tab(1)
|
||||
assert tabs_manager.content_container.content == tabs_manager.manager.tabs[1]["content"]
|
||||
assert (
|
||||
tabs_manager.content_container.content
|
||||
== tabs_manager.manager.tabs[1]["content"]
|
||||
)
|
||||
|
||||
tabs_manager.select_tab(2)
|
||||
assert tabs_manager.content_container.content == tabs_manager.manager.tabs[2]["content"]
|
||||
assert (
|
||||
tabs_manager.content_container.content
|
||||
== tabs_manager.manager.tabs[2]["content"]
|
||||
)
|
||||
|
||||
@@ -28,7 +28,9 @@ class TestBuildUI:
|
||||
@patch("ren_browser.pages.page_request.PageFetcher")
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_appbar_setup(self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page):
|
||||
def test_build_ui_appbar_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
):
|
||||
"""Test that build_ui sets up the app bar correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
mock_tabs.return_value = mock_tab_manager
|
||||
@@ -48,7 +50,9 @@ class TestBuildUI:
|
||||
@patch("ren_browser.pages.page_request.PageFetcher")
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_drawer_setup(self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page):
|
||||
def test_build_ui_drawer_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
):
|
||||
"""Test that build_ui sets up the drawer correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
mock_tabs.return_value = mock_tab_manager
|
||||
@@ -116,9 +120,10 @@ class TestOpenSettingsTab:
|
||||
mock_tab_manager._add_tab_internal = Mock()
|
||||
mock_tab_manager.select_tab = Mock()
|
||||
|
||||
with patch("pathlib.Path.read_text", return_value="config"), \
|
||||
patch("pathlib.Path.write_text"):
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.read_text", return_value="config"),
|
||||
patch("pathlib.Path.write_text"),
|
||||
):
|
||||
open_settings_tab(mock_page, mock_tab_manager)
|
||||
|
||||
# Get the settings content that was added
|
||||
@@ -129,7 +134,10 @@ class TestOpenSettingsTab:
|
||||
for control in settings_content.controls:
|
||||
if hasattr(control, "controls"):
|
||||
for sub_control in control.controls:
|
||||
if hasattr(sub_control, "text") and sub_control.text == "Save and Restart":
|
||||
if (
|
||||
hasattr(sub_control, "text")
|
||||
and sub_control.text == "Save and Restart"
|
||||
):
|
||||
save_btn = sub_control
|
||||
break
|
||||
|
||||
@@ -142,7 +150,10 @@ class TestOpenSettingsTab:
|
||||
mock_tab_manager._add_tab_internal = Mock()
|
||||
mock_tab_manager.select_tab = Mock()
|
||||
|
||||
with patch('ren_browser.ui.settings.get_storage_manager', return_value=mock_storage_manager):
|
||||
with patch(
|
||||
"ren_browser.ui.settings.get_storage_manager",
|
||||
return_value=mock_storage_manager,
|
||||
):
|
||||
open_settings_tab(mock_page, mock_tab_manager)
|
||||
|
||||
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
|
||||
@@ -155,10 +166,14 @@ class TestOpenSettingsTab:
|
||||
mock_tab_manager._add_tab_internal = Mock()
|
||||
mock_tab_manager.select_tab = Mock()
|
||||
|
||||
with patch('ren_browser.ui.settings.get_storage_manager', return_value=mock_storage_manager), \
|
||||
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]), \
|
||||
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]):
|
||||
|
||||
with (
|
||||
patch(
|
||||
"ren_browser.ui.settings.get_storage_manager",
|
||||
return_value=mock_storage_manager,
|
||||
),
|
||||
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]),
|
||||
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]),
|
||||
):
|
||||
open_settings_tab(mock_page, mock_tab_manager)
|
||||
|
||||
mock_tab_manager._add_tab_internal.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user