stupidpythonbrowser 4.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pybrowser/settings.py ADDED
@@ -0,0 +1,656 @@
1
+ """
2
+ PyBrowser Settings & Configuration Module.
3
+ Handles settings storage, directory resolution, and the Material 3 Settings Page.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import time
9
+ import urllib.parse
10
+ import re
11
+ import html
12
+ import tempfile
13
+ import shutil
14
+
15
+ UA_PRESETS = {
16
+ "safari_18": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
17
+ "chrome_mac": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
18
+ "chrome_win": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
19
+ "firefox_mac": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
20
+ }
21
+ DEFAULT_URL = "https://www.google.com"
22
+
23
+ DATA_DIR = os.path.expanduser("~/data")
24
+ os.makedirs(DATA_DIR, exist_ok=True)
25
+
26
+ _cur_dir = os.path.dirname(os.path.abspath(__file__))
27
+ _old_candidates = [
28
+ os.path.abspath(os.path.join(_cur_dir, "..", "browser_data")),
29
+ os.path.abspath(os.path.join(os.getcwd(), "browser_data")),
30
+ ]
31
+ for _old_dir in _old_candidates:
32
+ if os.path.exists(_old_dir):
33
+ for _fname in ["settings.json", "bookmarks.json", "session.json", "history.db", "adblock_hosts.txt", "dns_cache.json"]:
34
+ _src = os.path.join(_old_dir, _fname)
35
+ _dst = os.path.join(DATA_DIR, _fname)
36
+ if os.path.exists(_src) and not os.path.exists(_dst):
37
+ try:
38
+ shutil.copy2(_src, _dst)
39
+ except Exception:
40
+ pass
41
+ break
42
+
43
+ SESSION_FILE = os.path.join(DATA_DIR, "session.json")
44
+ BOOKMARKS_FILE = os.path.join(DATA_DIR, "bookmarks.json")
45
+ SETTINGS_FILE = os.path.join(DATA_DIR, "settings.json")
46
+ HISTORY_FILE = os.path.join(DATA_DIR, "history.db")
47
+ ADBLOCK_CACHE_FILE = os.path.join(DATA_DIR, "adblock_hosts.txt")
48
+ DNS_CACHE_FILE = os.path.join(DATA_DIR, "dns_cache.json")
49
+ USERSCRIPTS_DIR = os.path.join(DATA_DIR, "userscripts")
50
+ os.makedirs(USERSCRIPTS_DIR, exist_ok=True)
51
+
52
+ FILTER_LIST_URL = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
53
+ LOCAL_PROXY_PORT = 8888
54
+
55
+ BASE_ADBLOCK_DOMAINS = {
56
+ "doubleclick.net", "googleadservices.com", "googlesyndication.com",
57
+ "adsystem.com", "tracker.com", "analytics.google.com", "scorecardresearch.com",
58
+ "quantserve.com", "zedo.com", "adbrite.com", "amazon-adsystem.com", "criteo.com",
59
+ "taboola.com", "outbrain.com", "adroll.com", "adnxs.com", "rubiconproject.com",
60
+ "pubmatic.com", "openx.net", "casalemedia.com", "serving-sys.com", "moatads.com"
61
+ }
62
+
63
+ M3_COLORS = {
64
+ "tabstrip-bg": ("#DFE3E8", "#1F1F1F"),
65
+ "tabstrip-incognito": ("#141416", "#141416"),
66
+ "active-tab-bg": ("#FFFFFF", "#2D2F31"),
67
+ "inactive-tab-hover": ("rgba(0,0,0,0.08)", "rgba(255,255,255,0.08)"),
68
+ "toolbar-bg": ("#FFFFFF", "#282A2C"),
69
+ "toolbar-incognito": ("#1F1F21", "#1F1F21"),
70
+ "toolbar-separator": ("rgba(0,0,0,0.08)", "rgba(255,255,255,0.06)"),
71
+ "omnibox-bg-rest": ("#EDF2F7", "#1E1F22"),
72
+ "omnibox-bg-focus": ("#FFFFFF", "#1E1F22"),
73
+ "omnibox-text-host": ("#1F1F1F", "#E3E3E3"),
74
+ "omnibox-text-path": ("#757575", "#8E918F"),
75
+ "tab-divider": ("rgba(0,0,0,0.15)", "rgba(255,255,255,0.15)"),
76
+ }
77
+
78
+ GROUP_COLORS = {
79
+ "Grey": "#5F6368", "Blue": "#1A73E8", "Red": "#D93025", "Yellow": "#F9AB00",
80
+ "Green": "#1E8E3E", "Pink": "#E52592", "Purple": "#9334E6", "Cyan": "#12B5CB"
81
+ }
82
+
83
+ def validate_and_get_download_dir(target_path=None):
84
+ if not target_path:
85
+ target_path = SETTINGS.settings.get("download_path", os.path.expanduser("~/Downloads"))
86
+ expanded = os.path.expanduser(str(target_path).strip())
87
+ if expanded.startswith("/Volumes/") and not os.path.exists(expanded):
88
+ print(f"[PyBrowser] Warning: External volume at '{expanded}' is unmounted. Falling back to ~/Downloads.")
89
+ return os.path.expanduser("~/Downloads")
90
+ try:
91
+ os.makedirs(expanded, exist_ok=True)
92
+ if os.access(expanded, os.W_OK):
93
+ return expanded
94
+ except Exception as e:
95
+ print(f"[PyBrowser] Warning: Cannot write to '{expanded}': {e}. Falling back to ~/Downloads.")
96
+ return os.path.expanduser("~/Downloads")
97
+
98
+ def sanitize_suggested_filename(img_url, default="image.png"):
99
+ try:
100
+ parsed = urllib.parse.urlparse(img_url)
101
+ p_name = os.path.basename(parsed.path)
102
+ if p_name and "." in p_name:
103
+ filename = p_name.split("?")[0]
104
+ else:
105
+ filename = f"image_{int(time.time())}.png"
106
+ return filename
107
+ except Exception:
108
+ return default
109
+
110
+ class SettingsManager:
111
+ def __init__(self):
112
+ self.defaults = {
113
+ "search_engine_preset": "google",
114
+ "custom_search_url": "https://www.google.com/search?q=%s",
115
+ "search_in_new_tab": False,
116
+ "omnibox_suggestions_enabled": True,
117
+ "startup_behavior": "new_tab",
118
+ "startup_url": "https://www.google.com",
119
+ "startup_window_mode": "normal",
120
+ "downloads_enabled": True,
121
+ "download_path": os.path.expanduser("~/Downloads"),
122
+ "js_enabled": True,
123
+ "adblock_enabled": True,
124
+ "adblock_stevenblack_enabled": True,
125
+ "adblock_base_enabled": True,
126
+ "cosmetic_adblock_enabled": True,
127
+ "yt_adblock_enabled": True,
128
+ "bookmarks_bar_visible": True,
129
+ "vertical_tabs_enabled": False,
130
+ "vertical_tabs_collapsed": False,
131
+ "show_home_button": False,
132
+ "home_url": "https://www.google.com",
133
+ "default_zoom": 0.8,
134
+ "doh_provider": "cloudflare",
135
+ "doh_custom_url": "",
136
+ "custom_adblock_domains": "",
137
+ "adblock_last_updated": "Built-in Lists",
138
+ "theme_mode": "system",
139
+ "https_only": True,
140
+ "block_popups": True,
141
+ "webgl_enabled": True,
142
+ "user_agent_mode": "safari_18",
143
+ "custom_user_agent": "",
144
+ "tab_bar_double_click_action": "maximize",
145
+ "saved_logins": {},
146
+ "history_enabled": True
147
+ }
148
+ self.settings = dict(self.defaults)
149
+ self.load()
150
+
151
+ def load(self):
152
+ if os.path.exists(SETTINGS_FILE):
153
+ try:
154
+ with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
155
+ saved_data = json.load(f)
156
+ if isinstance(saved_data, dict):
157
+ self.settings.update(saved_data)
158
+ except Exception as e:
159
+ print(f"[Settings Load Warning] {e}")
160
+ try:
161
+ z = float(self.settings.get("default_zoom", 0.8))
162
+ self.settings["default_zoom"] = max(0.5, min(2.0, z))
163
+ except Exception:
164
+ self.settings["default_zoom"] = 0.8
165
+ self.save()
166
+
167
+ def save(self):
168
+ try:
169
+ dir_name = os.path.dirname(SETTINGS_FILE) or "."
170
+ fd, temp_path = tempfile.mkstemp(dir=dir_name)
171
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
172
+ json.dump(self.settings, f, indent=2)
173
+ os.replace(temp_path, SETTINGS_FILE)
174
+ except Exception as e:
175
+ print(f"[Settings Save Error] {e}")
176
+
177
+ def reset_defaults(self):
178
+ self.settings = dict(self.defaults)
179
+ self.save()
180
+
181
+ def get_search_url(self, query):
182
+ preset = self.settings.get("search_engine_preset", "google")
183
+ encoded = urllib.parse.quote(query)
184
+ if preset == "custom":
185
+ c = self.settings.get("custom_search_url", "").strip()
186
+ if c:
187
+ return c.replace("%s", encoded) if "%s" in c else (c + encoded)
188
+ presets = {
189
+ "google": "https://www.google.com/search?q=",
190
+ "duckduckgo": "https://duckduckgo.com/?q=",
191
+ "bing": "https://www.bing.com/search?q=",
192
+ "brave": "https://search.brave.com/search?q=",
193
+ "ecosia": "https://www.ecosia.org/search?q=",
194
+ "kagi": "https://kagi.com/search?q="
195
+ }
196
+ return presets.get(preset, "https://www.google.com/search?q=") + encoded
197
+
198
+ def get_custom_adblock_domains(self):
199
+ domains = set()
200
+ custom = self.settings.get("custom_adblock_domains", "")
201
+ if custom:
202
+ for d in re.split(r'[\r\n,]+', custom):
203
+ d = d.strip().lower()
204
+ if d:
205
+ domains.add(d)
206
+ return domains
207
+
208
+ def get_user_agent(self):
209
+ mode = self.settings.get("user_agent_mode", "safari_18")
210
+ if mode == "custom":
211
+ return self.settings.get("custom_user_agent", "").strip() or UA_PRESETS["safari_18"]
212
+ return UA_PRESETS.get(mode, UA_PRESETS["safari_18"])
213
+
214
+ SETTINGS = SettingsManager()
215
+
216
+ def get_settings_html(sb_count, base_count, custom_count, active_rules_count):
217
+ s = SETTINGS.settings
218
+ engine_preset = s.get("search_engine_preset", "google")
219
+ custom_search_url = s.get("custom_search_url", "https://www.google.com/search?q=%s")
220
+ doh_provider = s.get("doh_provider", "cloudflare")
221
+ doh_custom_url = s.get("doh_custom_url", "")
222
+ startup_behavior = s.get("startup_behavior", "new_tab")
223
+ startup_window_mode = s.get("startup_window_mode", "normal")
224
+ theme_mode = s.get("theme_mode", "system")
225
+ ua_mode = s.get("user_agent_mode", "safari_18")
226
+ tab_action = s.get("tab_bar_double_click_action", "maximize")
227
+ downloads_enabled = s.get("downloads_enabled", True)
228
+ cur_dl_path = s.get("download_path", os.path.expanduser("~/Downloads"))
229
+ cur_zoom = str(s.get("default_zoom", 1.0))
230
+
231
+ return f"""<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Settings</title>
232
+ <style>
233
+ :root {{ --bg:#F8F9FA; --surface:#FFFFFF; --sidebar-bg:#F8F9FA; --text:#1F1F1F; --text-sec:#5E6368; --border:#E0E2E0; --border-subtle:#F1F3F4; --primary:#1A73E8; --primary-hover:#1557B0; --primary-container:#D3E3FD; --on-primary-container:#041E49; --card-shadow:0 1px 3px rgba(0,0,0,0.05); --input-bg:#FFFFFF; --track-off:#C4C7C5; --thumb-off:#747775; }}
234
+ @media (prefers-color-scheme:dark) {{ :root {{ --bg:#1A1A1C; --surface:#282A2C; --sidebar-bg:#1A1A1C; --text:#E3E3E3; --text-sec:#9AA0A6; --border:rgba(255,255,255,0.08); --border-subtle:rgba(255,255,255,0.04); --primary:#8AB4F8; --primary-hover:#A8C7FA; --primary-container:#004A77; --on-primary-container:#D3E3FD; --card-shadow:0 1px 4px rgba(0,0,0,0.3); --input-bg:#1F1F21; --track-off:#444746; --thumb-off:#8E918F; }} }}
235
+ * {{ box-sizing:border-box; margin:0; padding:0; }}
236
+ body {{ font-family:-apple-system,sans-serif; background:var(--bg); color:var(--text); display:flex; height:100vh; overflow:hidden; user-select:none; }}
237
+ #sidebar {{ width:270px; min-width:270px; background:var(--sidebar-bg); border-right:1px solid var(--border); padding:24px 16px; display:flex; flex-direction:column; overflow-y:auto; }}
238
+ .app-title {{ font-size:19px; font-weight:600; padding:0 14px 20px 14px; display:flex; align-items:center; gap:10px; color:var(--text); border-bottom:1px solid var(--border); margin-bottom:14px; }}
239
+ .nav-item {{ display:flex; align-items:center; gap:12px; padding:10px 16px; font-size:13.5px; font-weight:500; border-radius:20px; cursor:pointer; color:var(--text-sec); transition:background 0.15s, color 0.15s; margin-bottom:4px; }}
240
+ .nav-item:hover {{ background:rgba(0,0,0,0.04); color:var(--text); }}
241
+ .nav-item.active {{ background:var(--primary-container); color:var(--on-primary-container); font-weight:600; }}
242
+ #content {{ flex:1; overflow-y:auto; padding:36px 48px; scroll-behavior:smooth; }}
243
+ .container {{ max-width:820px; margin:0 auto; }}
244
+ .section {{ margin-bottom:34px; }}
245
+ .section-title {{ font-size:16px; font-weight:600; margin-bottom:14px; color:var(--text); display:flex; align-items:center; gap:8px; }}
246
+ .card {{ background:var(--surface); border-radius:16px; border:1px solid var(--border); box-shadow:var(--card-shadow); }}
247
+ .row {{ display:flex; justify-content:space-between; align-items:center; padding:14px 20px; border-bottom:1px solid var(--border-subtle); }}
248
+ .row:last-child {{ border-bottom:none; }}
249
+ .row-col {{ display:flex; flex-direction:column; padding:16px 20px; border-bottom:1px solid var(--border-subtle); gap:10px; }}
250
+ .label-group {{ display:flex; flex-direction:column; gap:3px; max-width:65%; }}
251
+ .label-title {{ font-size:13.5px; font-weight:500; }}
252
+ .label-desc {{ font-size:12px; color:var(--text-sec); line-height:1.4; }}
253
+ .switch {{ position:relative; display:inline-block; width:44px; height:24px; }}
254
+ .switch input {{ opacity:0; width:0; height:0; }}
255
+ .slider {{ position:absolute; cursor:pointer; inset:0; background-color:var(--track-off); transition:0.2s; border-radius:24px; }}
256
+ .slider:before {{ position:absolute; content:""; height:16px; width:16px; left:4px; bottom:4px; background-color:var(--thumb-off); transition:0.2s; border-radius:50%; }}
257
+ input:checked + .slider {{ background-color:var(--primary); }}
258
+ input:checked + .slider:before {{ transform:translateX(20px); background-color:#FFFFFF; }}
259
+ .m3-select, .m3-input {{ background:var(--input-bg); border:1px solid var(--border); border-radius:8px; padding:8px 12px; font-size:13px; color:var(--text); outline:none; }}
260
+ .btn {{ background:var(--primary); color:#FFF; border:none; border-radius:18px; padding:8px 18px; font-size:13px; font-weight:500; cursor:pointer; }}
261
+ .btn:hover {{ background:var(--primary-hover); }}
262
+ .btn-danger {{ background:#D93025; color:#FFF; }}
263
+ #toast {{ position:fixed; bottom:24px; right:24px; background:#323232; color:#FFF; padding:12px 22px; border-radius:10px; font-size:13px; opacity:0; transition:opacity 0.25s, transform 0.25s; pointer-events:none; transform:translateY(10px); z-index:9999; }}
264
+ #toast.show {{ opacity:1; transform:translateY(0); }}
265
+ .badge {{ display:inline-block; padding:3px 8px; font-size:11px; font-weight:600; border-radius:10px; background:var(--primary-container); color:var(--on-primary-container); }}
266
+ .check-row {{ display:flex; align-items:center; gap:10px; margin-bottom:8px; font-size:13px; }}
267
+ .rule-inspector-box {{ background:var(--bg); border:1px solid var(--border); border-radius:10px; padding:14px; margin-top:10px; font-size:12px; max-height:180px; overflow-y:auto; font-family:monospace; }}
268
+ </style></head><body>
269
+ <div id="sidebar">
270
+ <div class="app-title">⚙️ Settings</div>
271
+ <div class="nav-item active" onclick="jumpTo('sec-search', this)">🔍 Search & Omnibox</div>
272
+ <div class="nav-item" onclick="jumpTo('sec-appearance', this)">🎨 Appearance & Theme</div>
273
+ <div class="nav-item" onclick="jumpTo('sec-privacy', this)">🛡️ uBlock & Privacy</div>
274
+ <div class="nav-item" onclick="jumpTo('sec-general', this)">⚡ General & Startup</div>
275
+ <div class="nav-item" onclick="jumpTo('sec-downloads', this)">⬇️ Downloads & Drives</div>
276
+ <div class="nav-item" onclick="jumpTo('sec-network', this)">🌐 Network & Advanced</div>
277
+ <div class="nav-item" onclick="jumpTo('sec-clear', this)">🧹 Clear Browsing Data</div>
278
+ <div class="nav-item" onclick="jumpTo('sec-reset', this)">🔄 Reset Defaults</div>
279
+ </div>
280
+ <div id="content"><div class="container">
281
+
282
+ <div id="sec-search" class="section">
283
+ <div class="section-title">🔍 Search & Omnibox</div>
284
+ <div class="card">
285
+ <div class="row">
286
+ <div class="label-group"><div class="label-title">Default Search Provider</div><div class="label-desc">Used automatically when non-URL keywords are typed</div></div>
287
+ <select class="m3-select" onchange="onSearchPresetChange(this.value)">
288
+ <option value="google" {'selected' if engine_preset == 'google' else ''}>Google</option>
289
+ <option value="duckduckgo" {'selected' if engine_preset == 'duckduckgo' else ''}>DuckDuckGo</option>
290
+ <option value="bing" {'selected' if engine_preset == 'bing' else ''}>Bing</option>
291
+ <option value="brave" {'selected' if engine_preset == 'brave' else ''}>Brave</option>
292
+ <option value="ecosia" {'selected' if engine_preset == 'ecosia' else ''}>Ecosia</option>
293
+ <option value="kagi" {'selected' if engine_preset == 'kagi' else ''}>Kagi</option>
294
+ <option value="custom" {'selected' if engine_preset == 'custom' else ''}>Custom Template</option>
295
+ </select>
296
+ </div>
297
+ <div id="custom-search-row" class="row" style="display: {'flex' if engine_preset == 'custom' else 'none'};">
298
+ <div class="label-group"><div class="label-title">Custom Search URL Template</div><div class="label-desc">Use <code>%s</code> as query placeholder</div></div>
299
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(custom_search_url)}" onchange="saveSetting('custom_search_url', this.value, 'Custom search URL saved')">
300
+ </div>
301
+ <div class="row">
302
+ <div class="label-group"><div class="label-title">Search in New Tab</div><div class="label-desc">Open Omnibox search results in a new tab</div></div>
303
+ <label class="switch">
304
+ <input type="checkbox" {'checked' if s.get('search_in_new_tab', False) else ''} onchange="saveSetting('search_in_new_tab', this.checked, 'Search target updated')">
305
+ <span class="slider"></span>
306
+ </label>
307
+ </div>
308
+ <div class="row">
309
+ <div class="label-group"><div class="label-title">Omnibox Suggestions</div><div class="label-desc">Display search & history autocomplete dropdown</div></div>
310
+ <label class="switch">
311
+ <input type="checkbox" {'checked' if s.get('omnibox_suggestions_enabled', True) else ''} onchange="saveSetting('omnibox_suggestions_enabled', this.checked, 'Suggestions toggled')">
312
+ <span class="slider"></span>
313
+ </label>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div id="sec-appearance" class="section">
319
+ <div class="section-title">🎨 Appearance & Window</div>
320
+ <div class="card">
321
+ <div class="row">
322
+ <div class="label-group"><div class="label-title">Theme Mode</div><div class="label-desc">Color palette for browser controls & UI</div></div>
323
+ <select class="m3-select" onchange="saveSetting('theme_mode', this.value, 'Theme updated')">
324
+ <option value="system" {'selected' if theme_mode == 'system' else ''}>System Default</option>
325
+ <option value="light" {'selected' if theme_mode == 'light' else ''}>Light</option>
326
+ <option value="dark" {'selected' if theme_mode == 'dark' else ''}>Dark</option>
327
+ </select>
328
+ </div>
329
+ <div class="row">
330
+ <div class="label-group"><div class="label-title">Default Zoom Level</div><div class="label-desc">Default page scaling factor for new web views</div></div>
331
+ <select class="m3-select" onchange="saveSetting('default_zoom', parseFloat(this.value), 'Zoom level updated')">
332
+ <option value="0.75" {'selected' if cur_zoom == '0.75' else ''}>75%</option>
333
+ <option value="0.8" {'selected' if cur_zoom in ('0.8', '0.80') else ''}>80%</option>
334
+ <option value="1.0" {'selected' if cur_zoom in ('1', '1.0', '1.00') else ''}>100% (Default)</option>
335
+ <option value="1.25" {'selected' if cur_zoom == '1.25' else ''}>125%</option>
336
+ <option value="1.5" {'selected' if cur_zoom == '1.5' else ''}>150%</option>
337
+ </select>
338
+ </div>
339
+ <div class="row">
340
+ <div class="label-group"><div class="label-title">Vertical Tabs</div><div class="label-desc">Display tabs in a collapsible sidebar on the left</div></div>
341
+ <label class="switch">
342
+ <input type="checkbox" {'checked' if s.get('vertical_tabs_enabled', False) else ''} onchange="saveSetting('vertical_tabs_enabled', this.checked, 'Vertical tabs toggled')">
343
+ <span class="slider"></span>
344
+ </label>
345
+ </div>
346
+ <div class="row">
347
+ <div class="label-group"><div class="label-title">Tab Bar Double-Click</div><div class="label-desc">Action when empty tab bar area is double-clicked</div></div>
348
+ <select class="m3-select" onchange="saveSetting('tab_bar_double_click_action', this.value, 'Tab action saved')">
349
+ <option value="maximize" {'selected' if tab_action == 'maximize' else ''}>Zoom / Maximize</option>
350
+ <option value="fullscreen" {'selected' if tab_action == 'fullscreen' else ''}>Toggle Fullscreen</option>
351
+ </select>
352
+ </div>
353
+ <div class="row">
354
+ <div class="label-group"><div class="label-title">Show Home Button</div><div class="label-desc">Display toolbar button pointing to home URL</div></div>
355
+ <label class="switch">
356
+ <input type="checkbox" {'checked' if s.get('show_home_button', False) else ''} onchange="saveSetting('show_home_button', this.checked, 'Home button toggled')">
357
+ <span class="slider"></span>
358
+ </label>
359
+ </div>
360
+ <div class="row">
361
+ <div class="label-group"><div class="label-title">Home Page URL</div><div class="label-desc">Webpage loaded on clicking home button</div></div>
362
+ <input type="text" class="m3-input" style="width:260px;" value="{html.escape(s.get('home_url', 'https://www.google.com'))}" onchange="saveSetting('home_url', this.value, 'Home URL saved')">
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <div id="sec-privacy" class="section">
368
+ <div class="section-title">🛡️ uBlock-Grade Privacy & Adblock <span class="badge" id="adblock-count-badge">{active_rules_count:,} rules</span></div>
369
+ <div class="card">
370
+ <div class="row">
371
+ <div class="label-group"><div class="label-title">Master Adblock</div><div class="label-desc">Network rule lists and element blocker</div></div>
372
+ <label class="switch">
373
+ <input type="checkbox" {'checked' if s.get('adblock_enabled', True) else ''} onchange="saveSetting('adblock_enabled', this.checked, 'Adblock toggled')">
374
+ <span class="slider"></span>
375
+ </label>
376
+ </div>
377
+ <div class="row">
378
+ <div class="label-group"><div class="label-title">StevenBlack Unified Hosts ({sb_count:,} domains)</div><div class="label-desc">Community tracker and malware blocklists</div></div>
379
+ <label class="switch">
380
+ <input type="checkbox" {'checked' if s.get('adblock_stevenblack_enabled', True) else ''} onchange="saveSetting('adblock_stevenblack_enabled', this.checked, 'StevenBlack toggled')">
381
+ <span class="slider"></span>
382
+ </label>
383
+ </div>
384
+ <div class="row">
385
+ <div class="label-group"><div class="label-title">Cosmetic Element Blocker</div><div class="label-desc">Hides sponsored banners and ad containers</div></div>
386
+ <label class="switch">
387
+ <input type="checkbox" {'checked' if s.get('cosmetic_adblock_enabled', True) else ''} onchange="saveSetting('cosmetic_adblock_enabled', this.checked, 'Cosmetic adblock toggled')">
388
+ <span class="slider"></span>
389
+ </label>
390
+ </div>
391
+ <div class="row">
392
+ <div class="label-group"><div class="label-title">YouTube Ad Shield</div><div class="label-desc">Auto-skips YouTube video advertisements & banners</div></div>
393
+ <label class="switch">
394
+ <input type="checkbox" {'checked' if s.get('yt_adblock_enabled', True) else ''} onchange="saveSetting('yt_adblock_enabled', this.checked, 'YouTube shield toggled')">
395
+ <span class="slider"></span>
396
+ </label>
397
+ </div>
398
+ <div class="row-col">
399
+ <div class="label-group" style="max-width: 100%;">
400
+ <div class="label-title">Rule & Domain Inspector</div>
401
+ <div class="label-desc">Inspect whether any domain is currently blocked and identify its rule list</div>
402
+ </div>
403
+ <div style="display:flex; gap:10px;">
404
+ <input type="text" id="inspector-query" class="m3-input" style="flex:1;" placeholder="Search blocked domain (e.g. doubleclick, tracker, analytics)..." onkeydown="if(event.key==='Enter') searchDomain()">
405
+ <button class="btn" onclick="searchDomain()">Inspect Domain</button>
406
+ </div>
407
+ <div id="inspector-results" class="rule-inspector-box" style="display:none;"></div>
408
+ </div>
409
+ <div class="row">
410
+ <div class="label-group"><div class="label-title">Record Browsing History</div><div class="label-desc">Save visited pages to local SQLite database</div></div>
411
+ <label class="switch">
412
+ <input type="checkbox" {'checked' if s.get('history_enabled', True) else ''} onchange="saveSetting('history_enabled', this.checked, 'History recording toggled')">
413
+ <span class="slider"></span>
414
+ </label>
415
+ </div>
416
+ <div class="row">
417
+ <div class="label-group"><div class="label-title">DNS over HTTPS (DoH) Provider</div><div class="label-desc">Encrypted DNS resolution bypassing ISP inspection</div></div>
418
+ <select class="m3-select" onchange="onDoHChange(this.value)">
419
+ <option value="cloudflare" {'selected' if doh_provider == 'cloudflare' else ''}>Cloudflare (1.1.1.1)</option>
420
+ <option value="cloudflare_security" {'selected' if doh_provider == 'cloudflare_security' else ''}>Cloudflare Security (Malware Blocking)</option>
421
+ <option value="google" {'selected' if doh_provider == 'google' else ''}>Google (8.8.8.8)</option>
422
+ <option value="quad9" {'selected' if doh_provider == 'quad9' else ''}>Quad9 (9.9.9.9)</option>
423
+ <option value="adguard" {'selected' if doh_provider == 'adguard' else ''}>AdGuard DNS</option>
424
+ <option value="custom" {'selected' if doh_provider == 'custom' else ''}>Custom Provider</option>
425
+ </select>
426
+ </div>
427
+ <div id="custom-doh-row" class="row" style="display: {'flex' if doh_provider == 'custom' else 'none'};">
428
+ <div class="label-group"><div class="label-title">Custom DoH URL</div><div class="label-desc">Must support DNS-over-HTTPS JSON API</div></div>
429
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(doh_custom_url)}" placeholder="https://..." onchange="saveSetting('doh_custom_url', this.value, 'Custom DoH URL saved')">
430
+ </div>
431
+ <div class="row">
432
+ <div class="label-group"><div class="label-title">HTTPS-Only Mode</div><div class="label-desc">Upgrades unencrypted HTTP requests to secure HTTPS</div></div>
433
+ <label class="switch">
434
+ <input type="checkbox" {'checked' if s.get('https_only', True) else ''} onchange="saveSetting('https_only', this.checked, 'HTTPS-only toggled')">
435
+ <span class="slider"></span>
436
+ </label>
437
+ </div>
438
+ <div class="row">
439
+ <div class="label-group"><div class="label-title">Update Blocklists</div><div class="label-desc">Last updated: <span id="adblock-update-date">{s.get('adblock_last_updated', 'Built-in')}</span></div></div>
440
+ <button class="btn" id="update-filters-btn" onclick="updateAdblockFilters()">Update Lists Now</button>
441
+ </div>
442
+ <div class="row-col">
443
+ <div class="label-group" style="max-width: 100%;">
444
+ <div class="label-title">Custom Blocked Domains</div>
445
+ <div class="label-desc">Add additional domains to block (comma- or line-separated)</div>
446
+ </div>
447
+ <textarea class="m3-input" style="width:100%; height:75px; resize:vertical;" placeholder="adserver.com&#10;tracker.telemetry.io" onchange="saveSetting('custom_adblock_domains', this.value, 'Custom domains saved')">{html.escape(s.get('custom_adblock_domains', ''))}</textarea>
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <div id="sec-general" class="section">
453
+ <div class="section-title">⚡ General & Startup</div>
454
+ <div class="card">
455
+ <div class="row">
456
+ <div class="label-group"><div class="label-title">Startup Behavior</div><div class="label-desc">What pages to open upon browser launch</div></div>
457
+ <select class="m3-select" onchange="onStartupChange(this.value)">
458
+ <option value="new_tab" {'selected' if startup_behavior == 'new_tab' else ''}>Open New Tab</option>
459
+ <option value="restore" {'selected' if startup_behavior == 'restore' else ''}>Restore Previous Session</option>
460
+ <option value="custom_url" {'selected' if startup_behavior == 'custom_url' else ''}>Open Specific URL</option>
461
+ </select>
462
+ </div>
463
+ <div id="custom-startup-row" class="row" style="display: {'flex' if startup_behavior == 'custom_url' else 'none'};">
464
+ <div class="label-group"><div class="label-title">Startup URL</div><div class="label-desc">Webpage loaded on initial application launch</div></div>
465
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('startup_url', 'https://www.google.com'))}" onchange="saveSetting('startup_url', this.value, 'Startup URL saved')">
466
+ </div>
467
+ <div class="row">
468
+ <div class="label-group"><div class="label-title">Window State on Launch</div><div class="label-desc">Window geometry upon application launch</div></div>
469
+ <select class="m3-select" onchange="saveSetting('startup_window_mode', this.value, 'Window launch mode saved')">
470
+ <option value="normal" {'selected' if startup_window_mode == 'normal' else ''}>Standard Window</option>
471
+ <option value="maximized" {'selected' if startup_window_mode == 'maximized' else ''}>Maximized</option>
472
+ <option value="fullscreen" {'selected' if startup_window_mode == 'fullscreen' else ''}>Full Screen</option>
473
+ </select>
474
+ </div>
475
+ <div class="row">
476
+ <div class="label-group"><div class="label-title">Enable JavaScript</div><div class="label-desc">Allow web pages to execute client-side scripts</div></div>
477
+ <label class="switch">
478
+ <input type="checkbox" {'checked' if s.get('js_enabled', True) else ''} onchange="saveSetting('js_enabled', this.checked, 'JavaScript setting updated')">
479
+ <span class="slider"></span>
480
+ </label>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <div id="sec-downloads" class="section">
486
+ <div class="section-title">⬇️ Downloads & Storage Devices</div>
487
+ <div class="card">
488
+ <div class="row">
489
+ <div class="label-group"><div class="label-title">Allow File Downloads</div><div class="label-desc">Permit websites and images to trigger downloads</div></div>
490
+ <label class="switch">
491
+ <input type="checkbox" {'checked' if downloads_enabled else ''} onchange="saveSetting('downloads_enabled', this.checked, 'Downloads toggled')">
492
+ <span class="slider"></span>
493
+ </label>
494
+ </div>
495
+ <div class="row">
496
+ <div class="label-group">
497
+ <div class="label-title">Download Location</div>
498
+ <div class="label-desc">Directory or mounted external drive (/Volumes/...) for saved files</div>
499
+ <div id="dl-path-display" style="font-size:11.5px; color:var(--primary); font-family:monospace; margin-top:4px;">{html.escape(cur_dl_path)}</div>
500
+ </div>
501
+ <div style="display:flex; gap:8px; align-items:center;">
502
+ <input type="text" id="dl-path-input" class="m3-input" style="width:240px;" value="{html.escape(cur_dl_path)}"
503
+ onchange="saveSetting('download_path', this.value, 'Download path saved')"
504
+ onkeydown="if(event.key==='Enter') saveSetting('download_path', this.value, 'Download path saved')">
505
+ <button class="btn" onclick="browseDownloadFolder()">Browse…</button>
506
+ </div>
507
+ </div>
508
+ </div>
509
+ </div>
510
+
511
+ <div id="sec-network" class="section">
512
+ <div class="section-title">🌐 Network & Advanced</div>
513
+ <div class="card">
514
+ <div class="row">
515
+ <div class="label-group"><div class="label-title">Browser Identity (User-Agent)</div><div class="label-desc">Emulate different browsers for compatibility</div></div>
516
+ <select class="m3-select" onchange="onUAModeChange(this.value)">
517
+ <option value="safari_18" {'selected' if ua_mode == 'safari_18' else ''}>Safari 18.0 Desktop (macOS Default)</option>
518
+ <option value="chrome_mac" {'selected' if ua_mode == 'chrome_mac' else ''}>Google Chrome (macOS)</option>
519
+ <option value="chrome_win" {'selected' if ua_mode == 'chrome_win' else ''}>Google Chrome (Windows)</option>
520
+ <option value="firefox_mac" {'selected' if ua_mode == 'firefox_mac' else ''}>Firefox (macOS)</option>
521
+ <option value="custom" {'selected' if ua_mode == 'custom' else ''}>Custom String</option>
522
+ </select>
523
+ </div>
524
+ <div id="custom-ua-row" class="row" style="display: {'flex' if ua_mode == 'custom' else 'none'};">
525
+ <div class="label-group"><div class="label-title">Custom User-Agent Header</div><div class="label-desc">Raw User-Agent string sent with requests</div></div>
526
+ <input type="text" class="m3-input" style="width:280px;" value="{html.escape(s.get('custom_user_agent', ''))}" onchange="saveSetting('custom_user_agent', this.value, 'Custom User-Agent saved')">
527
+ </div>
528
+ <div class="row">
529
+ <div class="label-group"><div class="label-title">WebGL Hardware Acceleration</div><div class="label-desc">Enable 3D canvas and accelerated GPU graphics</div></div>
530
+ <label class="switch">
531
+ <input type="checkbox" {'checked' if s.get('webgl_enabled', True) else ''} onchange="saveSetting('webgl_enabled', this.checked, 'WebGL setting updated')">
532
+ <span class="slider"></span>
533
+ </label>
534
+ </div>
535
+ </div>
536
+ </div>
537
+
538
+ <div id="sec-clear" class="section">
539
+ <div class="section-title">🧹 Clear Browsing Data</div>
540
+ <div class="card">
541
+ <div class="row-col">
542
+ <div class="check-row">
543
+ <input type="checkbox" id="clr-history" checked>
544
+ <label for="clr-history"><strong>Browsing History</strong> (Stored SQLite database visits)</label>
545
+ </div>
546
+ <div class="check-row">
547
+ <input type="checkbox" id="clr-cookies" checked>
548
+ <label for="clr-cookies"><strong>Cookies & Cache</strong> (Website storage and session cache)</label>
549
+ </div>
550
+ <div class="check-row">
551
+ <input type="checkbox" id="clr-favicons" checked>
552
+ <label for="clr-favicons"><strong>Favicon Cache</strong> (Memory site icons)</label>
553
+ </div>
554
+ <div class="check-row">
555
+ <input type="checkbox" id="clr-dns" checked>
556
+ <label for="clr-dns"><strong>Local DNS Cache</strong> (Resolved IP lookups)</label>
557
+ </div>
558
+ <div style="margin-top: 10px;">
559
+ <button class="btn btn-danger" onclick="executeClearData()">Clear Selected Data Now</button>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+
565
+ <div id="sec-reset" class="section">
566
+ <div class="section-title">🔄 Reset Defaults</div>
567
+ <div class="card">
568
+ <div class="row">
569
+ <div class="label-group"><div class="label-title">Restore Factory Settings</div><div class="label-desc">Revert search engine, theme, zoom, and flags to factory defaults</div></div>
570
+ <button class="btn btn-danger" onclick="executeResetDefaults()">Reset All Settings</button>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ </div></div>
576
+ <div id="toast">Settings updated</div>
577
+ <script>
578
+ function showToast(msg) {{
579
+ const t = document.getElementById('toast');
580
+ t.innerText = msg || 'Saved';
581
+ t.classList.add('show');
582
+ clearTimeout(window._toastTimer);
583
+ window._toastTimer = setTimeout(() => t.classList.remove('show'), 2400);
584
+ }}
585
+ function saveSetting(key, val, msg) {{
586
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'save_setting', key: key, val: val }});
587
+ showToast(msg);
588
+ }}
589
+ function onSearchPresetChange(val) {{
590
+ saveSetting('search_engine_preset', val, 'Search engine updated');
591
+ document.getElementById('custom-search-row').style.display = (val === 'custom') ? 'flex' : 'none';
592
+ }}
593
+ function onDoHChange(val) {{
594
+ saveSetting('doh_provider', val, 'DNS provider updated');
595
+ document.getElementById('custom-doh-row').style.display = (val === 'custom') ? 'flex' : 'none';
596
+ }}
597
+ function onUAModeChange(val) {{
598
+ saveSetting('user_agent_mode', val, 'User-Agent profile updated');
599
+ document.getElementById('custom-ua-row').style.display = (val === 'custom') ? 'flex' : 'none';
600
+ }}
601
+ function onStartupChange(val) {{
602
+ saveSetting('startup_behavior', val, 'Startup behavior updated');
603
+ document.getElementById('custom-startup-row').style.display = (val === 'custom_url') ? 'flex' : 'none';
604
+ }}
605
+ function browseDownloadFolder() {{
606
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'choose_download_directory' }});
607
+ }}
608
+ function updateAdblockFilters() {{
609
+ const b = document.getElementById('update-filters-btn');
610
+ b.innerText = "Syncing Lists..."; b.disabled = true;
611
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'update_adblock_filters' }});
612
+ }}
613
+ function searchDomain() {{
614
+ const q = document.getElementById('inspector-query').value.trim();
615
+ if (!q) return;
616
+ const box = document.getElementById('inspector-results');
617
+ box.style.display = 'block';
618
+ box.innerHTML = 'Searching blocklists...';
619
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'search_blocked_domains', query: q }});
620
+ }}
621
+ window.renderBlockedResults = function(jsonStr) {{
622
+ let items = [];
623
+ try {{ items = JSON.parse(jsonStr); }} catch(e) {{ items = []; }}
624
+ const box = document.getElementById('inspector-results');
625
+ if (!items || items.length === 0) {{
626
+ box.innerHTML = '<span style="color:#D93025;">No matches found. Domain is NOT currently blocked.</span>';
627
+ return;
628
+ }}
629
+ let out = '<div style="margin-bottom:6px;font-weight:600;color:var(--primary);">' + items.length + ' Matches Found:</div>';
630
+ items.forEach(it => {{
631
+ out += '<div><strong>[BLOCKED]</strong> ' + it.domain + ' <span style="opacity:0.6;">(' + it.source + ')</span></div>';
632
+ }});
633
+ box.innerHTML = out;
634
+ }};
635
+ function executeClearData() {{
636
+ const targets = [];
637
+ if (document.getElementById('clr-history').checked) targets.push('history');
638
+ if (document.getElementById('clr-cookies').checked) targets.push('cookies_cache');
639
+ if (document.getElementById('clr-favicons').checked) targets.push('favicons');
640
+ if (document.getElementById('clr-dns').checked) targets.push('dns_cache');
641
+ if (targets.length === 0) {{ showToast('Select at least one category'); return; }}
642
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'clear_browsing_data', targets: targets }});
643
+ showToast('Browsing data cleared');
644
+ }}
645
+ function executeResetDefaults() {{
646
+ if (confirm("Restore all browser settings to factory defaults?")) {{
647
+ window.webkit.messageHandlers.bridge.postMessage({{ cmd: 'reset_defaults' }});
648
+ }}
649
+ }}
650
+ function jumpTo(id, navBtn) {{
651
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
652
+ navBtn.classList.add('active');
653
+ const target = document.getElementById(id);
654
+ if (target) target.scrollIntoView({{ behavior: 'smooth' }});
655
+ }}
656
+ </script></body></html>"""